Do TypeScript conditional types map differently than constrained generics?

210 views Asked by At

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'>

Playground link

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?

1

There are 1 answers

1
Titian Cernicova-Dragomir On BEST ANSWER

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'
}

Playground Link