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

keyof result contains undefined when used on mapped type with optional properties #34992

Closed
vatosarmat opened this issue Nov 8, 2019 · 2 comments
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@vatosarmat
Copy link

I asked about that on stackoverflow and I was answered that the case seems to be buggy.
This happens only if strictNullChecks is enabled. It looks like the reason is that when we filter out properties in mapped type using conditional type and never, optional property becomes undefined instead of never. Normally Pick should reject union containing undefined but in below case it doesn't. Instead, it returns type with kinda hidden undefined key.
PickByValue copied from utility-types

TypeScript Version: 3.7.x-dev.201xxxxx
strictNullChecks

Search Terms:
keyof, undefined
Code

type PickByValue<T, ValueType> = Pick<T, {
    [Key in keyof T]: T[Key] extends ValueType ? Key : never;
}[keyof T]>;

type User = {
    name: string
    age?: number
}

// {name: string} - as expected
type UserStrings = PickByValue<User, string>

//'name' | undefined - where did `undefined` come from???
type UserStrngsKeys = keyof UserStrings


// optional `never` becomes `undefined` if --strictNullChecks enabled. Is that by design?
type SpecialUndefined = {
    foo?: never
}

Expected behavior:
Type UserStrngsKeys is 'name'

Actual behavior:
Type UserStrngsKeys is 'name' | undefined

Playground Link:
Playground Link

Related Issues:
Not found

@jack-williams
Copy link
Collaborator

The 'bug' here is that

{ [Key in keyof T]: T[Key] extends ValueType ? Key : never; }[keyof T]

is interpreted as satisfying the constraint keyof T, which is not true because the mapped type is homomorphic and undefined can creep into one of the property types. What you want is for
the previous mapped type { [Key in keyof T]: T[Key] extends ValueType ? Key : never; } to resolve to

type Example = {
    name: "name";
    age: never;
}

when T = User, so that Example[keyof User] = "name". What you get - because the mapped type is homomorphic - is:

type ExampleActual = {
    name: "name";
    age?: undefined;
}

and Example[keyof User] = "name" | undefined. This means that UserStrings resolves to the type:

Pick<User, "name" | undefined>

and applying keyof to this type just pulls out the supplied key "name" | undefined - despite the fact that this is an illegal type to begin with.

The 'correct' fix is that the definition of PickByValue should be illegal and give a compile error. I think the 'correct' definition of PickByValue is:

type PickByValue<T, ValueType> = Pick<T, {
    [Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T]>;

using the modifier -? to remove optionality and undefined.

@ahejlsberg
Copy link
Member

Given there's a simple solution that solves the problem, I'm inclined to do nothing further here. It's not clear how we would force an error as @jack-williams suggests, and I'm concerned that doing so would break existing code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants