8

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?

2
  • That's really interesting. And the answer to the title seems to be (from your code): Yes, they do. It's actually kind of cool to have both options. Note the difference if the union has a non-string in it ("a" | 42 | "b"). I have no idea why or what the rules are, though. :-) Commented Oct 24, 2020 at 9:40
  • The logic behind it seems pretty logical, like if you have a type type SeparateStrings<T> = T extends string ? T : never and you use it like SeparateStrings<'a' | 42 | 'b'> and expect yo get 'a' | 'b', it's logical what it does: it iterates over union types instead of working with this union as a single type. So such behaviour seems reasonable, no idea though, how to make it do what you want it to do, that's really interesting :p Commented Oct 24, 2020 at 9:45

1 Answer 1

4

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

Sign up to request clarification or add additional context in comments.

1 Comment

That's exactly what I was missing, thank you! The examples cited in the docs helped clarified that design decision, too. Appreciate the work around, as well - worked perfectly for me :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.