transform named tuple to object

TypeScript’s support for labeled tuple elements as implemented in microsoft/TypeScript#38234 intentionally does not provide any way to observe these labels in the type system. From the implementing pull request:

Names do not affect assignability in any way, and just exist for documentation and quickinfo.

Also see this comment in a related issue where the dev lead for the TS team says:

[Tuple labels] are purely for display purposes (similar to parameter names in function types)

The tuple type [a: string, b: number] and the tuple type [c: string, d: number] are indistinguishable from the point of view of the type system. The only difference between them is how they are displayed in an IDE or output as documentation. They are more like comments than code.

If [a: string, b: number] and [c: string, d: number] are the same type, then any type function TupleToObject<T> that turns [a: string, b: number] into {a: string, b: number} would also need to turn [c: string, d: number] into {a: string, b: number}.

So there’s really no hope of implementing what you’re looking for. Tuple labels are intentionally hidden from the type system. There’s just no connection between tuple labels and string literal types.


Instead, the only possible way forward is for you to manually specify the string literal keys you’d like to use for each element of the input tuple. So the unworkable TupleToObject<[a: string, b: number]> becomes something like TupleToObject<[a: string, b: number], ["a", "b"]>. You can see how this doesn’t care about tuple labels anymore; TupleToObject<[foo: string, bar: number], ["a", "b"]> would by necessity produce the same type.

Okay, so how would someone implement TupleToObject? Here’s one potential way:

type TupleToObject<T extends readonly any[],
  M extends Record<Exclude<keyof T, keyof any[]>, PropertyKey>> =
  { [K in Exclude<keyof T, keyof any[]> as M[K]]: T[K] };

It takes two type parameters: T is a meant to be the tuple type, while M is meant to be a tuple type of at least the same length whose values are keys. The extends readonly any[] and extends Record<Exclude<keyof T, keyof any[]>, PropertyKey>> generic constraints more or less enforce this.

Then for each literal numeric-like key of the T tuple, we use key remapping to replace that key with the corresponding entry from M. The only weird bit is that instead of iterating over keyof T, we use Exclude<keyof T, keyof any[]>. Tuple types are arrays, and will contain array method names like "map". We don’t want to worry about "map", so the Exclude<T, U> utility type lets us say “all those keys from T which are not general array keys”. For tuples this tends to leave just "0", "1", etc.


Let’s test it out:

type Tup = [a: string, b: number];

type Obj = TupleToObject<Tup, ["a", "b"]>
/* type Obj = {
    a: string;
    b: number;
} */

Looks good!

Playground link to code

Leave a Comment