Amlau
Amlau

Reputation: 53

Why are Typescript Mapped Tuple Types behaving differently when supplying Generic type vs direct type?

export type MyTuple = ["test", "othertest"];

type NotWorking = {
    [K in keyof MyTuple]: { value: MyTuple[K]};
};

type NotWorkingLengthType = NotWorking["length"]; // { value: 2 }

type Working<T>= {
    [P in keyof T]: { value: T[P] };
};

type MappedWorking = Working<MyTuple>;
type MappedWorkingLengthType = MappedWorking["length"]; // 2

Why is it behaving differently in that case ? That puzzles me.

Upvotes: 5

Views: 1572

Answers (2)

jcalz
jcalz

Reputation: 328433

This actually looks like a bug, see microsoft/TypeScript#27995.


Generally mapped types of the form {[K in keyof T]: ...T[K]...} are considered to be homomorphic mapped types, and the structure of the input type T is preserved as much as possible. This happens with optional and readonly keys (see microsoft/TypeScript#12563), and was also the intent when mapped tuples/arrays were implemented in microsoft/TypeScript#26063.

In order for that to work, it means the compiler must look at [K in keyof T] and remember to keep T around after it has evaluated keyof T. This happens when T is a generic type, and for optional/readonly keys this also happens when T is some concrete type:

type MyObj = { a?: string, readonly b: number };
type MyMappedObj = { [K in keyof MyObj]: { value: MyObj[K] } }
/* type MyMappedObj = {
    a?: {
        value: string | undefined;
    } | undefined;
    readonly b: {
        value: number;
    };
} */

Note that this only works when your mapped type is explicitly iterating over keys with exactly "in keyof". If you calculate your keys some other way, or assign them to a type alias, or even just parenthesize the keyof expression, the spell is broken and the mapped type is no longer homomorphic:

type MyBadMappedObj = { [K in (keyof MyObj)]: { value: MyObj[K] } }
/* type MyMappedObj = {
    a: { // not optional
        value: string | undefined; 
    } 
    b: { // not readonly
        value: number;
    };
} */

So, mapping over keys like {[K in keyof T]: ...} should preserve the structure of T in the output.


Unfortunately, when the mapped arrays/tuples feature was introduced, it looks like this was only implemented for when T is a generic type parameter, and not for a specific concrete type. In this comment, the implementer says:

The issue here is that we only map to tuple and array types when we instantiate a generic homomorphic mapped type for a tuple or array (see #26063). We should probably also do it for homomorphic mapped types with a keyof T where T is non-generic type.

and that's where it is for now. Maybe this will eventually be fixed. Until then, you should probably use an intermediate generic type like your Working example as a workaround.


Playground link to code

Upvotes: 4

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249706

Tuples and Array types are only preserved for homomorphic mapped types. A homomorphic mapped type is one that maps over keyof T, where T is a type parameter to the type, or K where K is a type parameter of the type, with K extends keyof T. This behavior is described in the PR that added preserving tuples and arrays in mapped types.

Without being specially preserved, when mapping a tuple all properties are mapped, including length and you get that less than useful result.

Upvotes: 0

Related Questions