Typescript: Conditional types fail to distribute in properties of mapped types

Created on 30 Sep 2019  路  5Comments  路  Source: microsoft/TypeScript


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.

Docs

Most helpful comment

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:

  • examples of naked and non-naked type parameters - "naked type parameter" is not explicitly defined anywhere, even though the definition seems obvious
  • mentioning the main counter-intuitive "gotcha" - that inlining a distributive conditional type alias may result in different behaviour
  • some examples of when conditional types are expected to be distributive, but actually aren't (the example above, #22945, #23046, #32274) - and how to work around it by using an additional type alias or extends infer.

All 5 comments

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.

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.

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.

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:

  • examples of naked and non-naked type parameters - "naked type parameter" is not explicitly defined anywhere, even though the definition seems obvious
  • mentioning the main counter-intuitive "gotcha" - that inlining a distributive conditional type alias may result in different behaviour
  • some examples of when conditional types are expected to be distributive, but actually aren't (the example above, #22945, #23046, #32274) - and how to work around it by using an additional type alias or extends infer.

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

You would be surprised! 馃槄

Was this page helpful?
0 / 5 - 0 ratings