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:
Since type aliases are equivalent to writing the expansion inline [...]
but that is not the case here.
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:
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! 馃槄
Most helpful comment
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:
extends infer.