Search code examples
typescript

Mapped types distributivity


I came across the following typescript challenge:

https://github.com/type-challenges/type-challenges/blob/main/questions/15260-hard-tree-path-array/README.md

Unable to figure out the solution myself I searched for the most popular solution:

type Path<T> = T extends Record<PropertyKey, unknown>
  ? {
      [P in keyof T]: [P, ...Path<T[P]>] | [P];
    }[keyof T]
  : never;

This works perfectly but it doesn't make sense to me. What troubles me here is that if we test the solution with an input like this:

type PathTest2 = Path<{ bar: { a: string }; baz: { b: number; c: number } }>;

we get this:

['bar'] | ['baz'] | ['bar', 'a'] | ['baz', 'b'] | ['baz', 'c']

...and I'm struggling to see how distributivity works after we get at this point:

{ baz: ['baz', ...['b'] | ['c']] | ['baz'] }

I'd get it if we passed a union to Path but that doesn't seems to be the case here. Any clues on what's going on?


Solution

  • Many (but not all) type operators in TypeScript are naturally distributive over unions, since many operations are naturally covariant (see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript).

    For example, indexed access types are distributive over unions in the key (e.g., T[K1 | K2] is T[K1] | T[K2]). There are also distributive conditional types.

    It turns out that variadic tuple types are also distributive over unions in the array/tuple type being spread. So if you have [A, B, C, ...(T1 | T2), X, Y, Z], this is transformed into [A, B, C, ...T1, X, Y, Z] | [A, B, C, ...T2, X, Y, Z]. This is documented in microsoft/TypeScript#39094, the pull request that implemented variadic tuple types:

    Instantiation of a tuple type with a variadic element ...T depends on the type argument provided for T [...] When the type argument for T is a union type, the union is spread over the tuple type. For example, [A, ...T, B] instantiated with X | Y | Z as the type argument for T yields a union of instantiations of [A, ...T, B] with X, Y and Z as the type argument for T respectively.

    So ['baz', ...['b'] | ['c']] is automatically transformed into ['baz', 'b'] | ['baz', 'c'].