Skip to content

Conditional types fail to distribute in properties of mapped types #33669

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

Open
mcpower opened this issue Sep 30, 2019 · 5 comments
Open

Conditional types fail to distribute in properties of mapped types #33669

mcpower opened this issue Sep 30, 2019 · 5 comments
Labels
Docs The issue relates to how you learn TypeScript

Comments

@mcpower
Copy link

mcpower commented Sep 30, 2019

TypeScript Version: 3.7.0-dev.20190928

Search Terms: conditional mapped property union

Code

type NullifyStrings<T> = T extends string ? null : T
type NullifyStringsInPropsWorking<T> = { [K in keyof T]: NullifyStrings<T[K]> }
type NullifyStringsInPropsBroken<T> = { [K in keyof T]: T[K] extends string ? null : T[K] }

type TestType = { a: number | string }
// { a: number | null }
type WorkingReplaceProps = NullifyStringsInPropsWorking<TestType>
// { a: string | number }
type BrokenReplaceProps = NullifyStringsInPropsBroken<TestType>

Expected behavior:
NullifyStringsInPropsWorking and NullifyStringsInPropsBroken should be functionally identical - expanding the NullifyStrings type alias in NullifyStringsInPropsWorking results in the same definition as NullifyStringsInPropsBroken.

Actual behavior:
NullifyStringsInPropsWorking and NullifyStringsInPropsBroken have different behaviour - BrokenReplaceProps has type { a: string | number } instead of the expected { a: number | null }.

Playground Link: Link

Related Issues:

#28339, but seems to be different.

#22945 mentions

Since type aliases are equivalent to writing the expansion inline [...]

but that is not the case here.

@mcpower
Copy link
Author

mcpower commented Sep 30, 2019

This issue is actually a duplicate of #22945!. The aforementioned issue can be worked around similarly (using Z<T> in other types):

type Id<T> = T;
type Z<T> = Id<T> extends true ? Id<T> : never;
type z = Z<boolean>;
type A<T extends any[]> = Z<T[0]>;
type a = A<[boolean]>;
type B<T extends {}> = { [P in keyof T]: Z<T[P]> }[keyof T];
type b = B<{a: boolean}>;

I'm not sure whether this issue should be closed as a result, but I still believe this is unexpected behaviour - that an "auxiliary" type is needed to write the types as intended, when type aliases should be equivalent to writing the expansion inline.

@mcpower
Copy link
Author

mcpower commented Sep 30, 2019

Another way of working around this is to use T[K] extends infer P and use P instead, like so:

type NullifyStringsInPropsWorking2<T> = {
    [K in keyof T]:
        T[K] extends infer P
        ? P extends string ? null : P
        : never
}

type TestType = { a: number | string }
// { a: number | null }
type WorkingReplaceProps2 = NullifyStringsInPropsWorking2<TestType>

This avoids the need for an auxiliary type.

@jack-williams
Copy link
Collaborator

jack-williams commented Sep 30, 2019

Duplicate of #23022, #23046, and #32274.

Regarding:

Since type aliases are equivalent to writing the expansion inline […]

the subtext is that this only applies when inlining a type alias instantiated with a type parameter.

Did you see this section of the handbook: distributive-conditional-types?

I don't mean this in a snide way --- lots of people have been tripped up on this and if they are reading the handbook and still getting confused then I think that is good to know.

@mcpower
Copy link
Author

mcpower commented Sep 30, 2019

Did you see this section of the handbook: distributive-conditional-types?

I don't mean this in a snide way --- lots of people have been tripped up on this and if they are reading the handbook and still getting confused then I think that is good to know.

I didn't, actually! After reading that section of the handbook, I think I still wouldn't have understood it - the section focuses on some nice working examples and doesn't show any examples for when things go wrong. IMO these things would be nice to have in the handbook for better understanding:

@fatcerberus
Copy link

The other misunderstanding, when conditional types are not expected to be distributive, but actually are (#23022), seems to be rarer.

You would be surprised! 😅

@RyanCavanaugh RyanCavanaugh added the Docs The issue relates to how you learn TypeScript label Oct 14, 2019
hpohlmeyer added a commit to hpohlmeyer/formik that referenced this issue Feb 6, 2020
Conditional types fail to distribute in properties of mapped types as
described in microsoft/TypeScript#33669
#issuecomment-536493169. This uses the infer approach to avoid
needing an auxiliary type to fix the issue.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Docs The issue relates to how you learn TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants