Reputation: 105
I am having no luck understanding why the code below functions as it does:
type MapOverString<T extends string> = { [K in T]: K };
type IfStringMapOverIt<T> = T extends string ? MapOverString<T> : never;
type ThisWorks = MapOverString<'a'>;
// { a: 'a' }
type ThisAlsoWorks = IfStringMapOverIt<'a'>;
// { a: 'a' }
type Union = 'a' | 'b' | 'c';
type ThisWorksToo = MapOverString<Union>;
// { a: 'a', b: 'b', c: 'c' }
type ThisDoesnt = IfStringMapOverIt<Union>;
// MapOverString<'a'> | MapOverString<'b'> | MapOverString<'c'>
I must be missing something, because MapOverString
and IfStringMapOverIt
seem like they should function identically.
Ultimately, I am using string literals and generics to cascade through permutations of configuration types. For example, if you want StringConfig<T>
configured with options 'a' | 'b' | 'c'
:
type ConfigMap<T> = T extends number
? NumberConfig
: T extends string
? StringConfig<T>
: never
type MyConfig = ConfigMap<'a' | 'b' | 'c'> // so many sad faces
Could someone enlighten me? What's going on here?
Upvotes: 8
Views: 223
Reputation: 249706
This is an application of the distribution property of conditional types. A condition over naked type parameter, will trigger this behavior and T extends string
satisfies this. You might also see T extend T
or T extends any
or T extends unknown
used for this very reason, just to trigger distribution.
You can read more about distributive conditional types in the handbook
You can disable distribution by using a condition over a tuple [T] extends [string]
. The effect of this is similar to a regular condition, just since the type parameter is no longer naked distribution will be displayed.
type StringConfig<T extends string> = { [K in T]: K };
type NumberConfig ={}
type ConfigMap<T> = [T] extends [number]
? NumberConfig
: [T] extends [string]
? StringConfig<T>
: never
export type MyConfig = ConfigMap<'a' | 'b' | 'c'> // so many sad faces
let x:MyConfig = {
a:'a',
b:'b',
c: 'c'
}
Upvotes: 4