Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type is not inferred correctly from a dynamically created decoder with generics #930

Open
vjrasane opened this issue Dec 22, 2022 · 4 comments

Comments

@vjrasane
Copy link

vjrasane commented Dec 22, 2022

A bit cryptic description, I know, but maybe the following example clarifies the issue:

import { array, Decoder, object } from "decoders"

type GenericArrayContainer<F> = {
    value: F[]
}

// inferred type matches declared type
const genericArrayContainer = <F>(decoder: Decoder<F>): Decoder<GenericArrayContainer<F>> => object({
    value: array(decoder),
})

type GenericValueContainer<F> = {
    value: F
}

// inferred type does not match declared type
const genericValueContainer = <F>(decoder: Decoder<F>): Decoder<GenericValueContainer<F>> => object({
    value: decoder,
})

as commented, in the first case the type is inferred correctly when the generic value is wrapped in another decoder, but if the field is declared with the dynamically passed decoder directly, type inference no longer works.

This was tried with:

    "decoders": "^2.0.1",
    "typescript": "^4.9.4"

Let me know if there is another approach to achieving what I'm trying to do here or if any further info is required.

@nvie
Copy link
Owner

nvie commented Dec 22, 2022

Hmm, looks like you may have found a bug. I would expect your example to work correctly, indeed. Does your example work correctly at runtime? This may be a bug in the signature type for object().

I'll have to take a deeper look at this issue later. Might be after the holidays before I have the proper time for it, but I'll try to look at it sooner! Thanks for reporting this!

@nvie
Copy link
Owner

nvie commented Jan 2, 2023

Just wanted to let you know that I've started to look into this, but a fix isn't trivial, and I need a bit more time to come up with a good solution for this problem (and also one that would work whether you have strictNullChecks enabled or disabled in your project).

The problem has to do with the fact that the F generic type could potentially be undefined, and there is some special treatment to undefined values within the object() decoder: if undefined is accepted by one of the field's decoders, the object() decoder will strip it from the output object. Since you cannot know if the provided decoder to your function will accept undefined values, the output type for it can potentially be either of:

{ value: F }

or

{ value?: F }

This is why TypeScript (correctly) fails. Unfortunately, it leads to a cryptic error message — totally unreadable!

To illustrate the issue further, here you can see the actual runtime behavior of the object() decoder at runtime if you would pass in an optional(string) as the value for your decoder:
https://runkit.com/nvie/63b2bc25b8699f0008779899

Therefore, you could fix this by changing the GenericValueContainer type definition to:

type GenericValueContainer<F> = {
    value?: F
//       ^ Note the question mark
}

This will make the error go away.

However, I realize this may not be ideal for your use case. To properly fix this, it may require the addition of a new decoder in the object() family of decoders (or some kind of "mode") that would not remove undefined keys from the object, and instead keep explicit-undefined keys around.

@vjrasane
Copy link
Author

vjrasane commented Jan 9, 2023

Ah yes that seems to be the issue. I missed the AllowImplicit in ObjectDecoderType while reading the typings. Fixing it might indeed be tricky if the implicit and explicit undefineds should be both allowed. In most cases these should be interchangeable, as far as I'm aware, but unfortunately in 'key' in obj they checks do behave differently so perhaps it's best to preserve the object decoder as it is. Instead maybe it would make sense to add a explicit function, that takes an object decoder (any of the object/exact/inexact) and returns a decoder that only accepts explicit undefined values in any of it's defined fields, although I'm not sure whether this is technically possible with the current implementation.

@github-actions github-actions bot added the Stale label Mar 11, 2023
Repository owner deleted a comment from github-actions bot Mar 11, 2023
@nvie nvie removed the Stale label Mar 11, 2023
@stevenyap
Copy link

stevenyap commented Apr 12, 2024

I hit into this issue too and I understand how difficult it is to be resolved by the maintainers.

Here's a workaround for now:

import * as JD from "decoders"

type GenericValueContainer<F> = {
  value: F
}

function genericValueContainerDecoder<F>(
  decoder: JD.Decoder<F>,
): JD.Decoder<GenericValueContainer<F>> {
  return JD.define((blob, ok, err) => {
    // Decode the wrapped field as an unknown first
    const result = JD.object({
      value: JD.unknown,
    }).decode(blob)

    if (result.ok === false) {
      return err(result.error)
    }

    // Decode the wrapped value manually
    const { value } = result.value
    const valueF = decoder.decode(value)
    return valueF.ok ? ok({ value: valueF.value }) : err(valueF.error)
  })
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants