Reputation: 1437
In TypeScript this is not compiling:
export interface Generic<T extends string> {
}
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: Generic<T[P]> }
}
Particularly, the Generic<T[P]>
fails with Type 'T[P]' does not satisfy the constraint 'string'.
. However, since T extends string[]
, it can be certain, that T[P] extends string
for any P in keyof T
.
What am I doing wrong here?
I know I can fix the problem with a conditional type:
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: T[P] extends string ? Generic<T[P]> : never }
}
But I don't see, why this should be necessary.
Upvotes: 0
Views: 242
Reputation: 8520
Notice that for T extends string[]
, keyof T
includes not only numeric keys but also all methods form Array's prototype.
Proof? Here it is:
type StringArrayKeys = keyof string[];
// Produces:
// type StringArrayKeys = number | "length" | "toString" | "toLocaleString" | "pop" |
// "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" |
// "unshift" | "indexOf" | "lastIndexOf" | ... 16 more
So the easiest fix for Your example would be to replace P in keyof T
with P in number
:
export interface Generic<T extends string> {};
export interface Class<T extends string[]> {
readonly prop: { [P in number]: Generic<T[P]> }
}
Upvotes: 0
Reputation: 329838
This is a known bug in TypeScript where the support (added in TS3.1) for mapped types over tuples and arrays does not exist inside the implementation of such mapped types; see microsoft/TypeScript#27995. It seems that according to the lead architect of TypeScript:
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).
So from the outside, when you use a mapped type on an array or tuple, the mapping preserves the array-or-tupleness of the input and only maps over numeric properties:
declare const foo: Class<["a", "b", "c"]>;
// (property) prop: [Generic<"a">, Generic<"b">, Generic<"c">]
const zero = foo.prop[0]; // Generic<"a">;
const one = foo.prop[1]; // Generic<"b">;
but on the inside, the compiler still sees P in keyof T
as iterating over every key of T
, including any possibly non-numeric ones.
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: Generic<T[P]> } // error!
}
As you note, there are workarounds for this, and these workarounds are mentioned in microsoft/TypeScript#27995. I think the best one is essentially the same as your conditional type:
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: Generic<Extract<T[P], string>> }
}
The other ones in there either don't work for generic types like T
, or produce mapped types that are no longer true arrays or tuples (e.g., {0: Generic<"a">, 1: Generic<"b">, 2: Generic<"c">}
instead of [Generic<"a">, Generic<"b">, Generic<"c">]
... so I'll leave them out of this answer.
Upvotes: 0
Reputation: 187232
If you look at the full error, the third line has a big clue:
Type 'T[P]' does not satisfy the constraint 'string'.
Type 'T[keyof T]' is not assignable to type 'string'.
Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string'.
Type 'T[string]' is not assignable to type 'string'.(2344)
The problem is that keyof
any array type (or tuple type) will be more like string | number | symbol
. And an array also has more than it's member type on those keys. For instance:
// (...items: string[]) => number
type PushFunction = string[]['push']
See this snippet. There's a lot more than just numbers in array keys:
// number | "0" | "1" | "2" | "length" | "toString"
// | "toLocaleString" | "pop" | "push" | "concat"
// | "join" | "reverse" | "shift" | "slice" | "sort"
// | "splice" | "unshift" | "indexOf"
// | ... 15 more ... | "includes"
type ArrayKeys = keyof [1,2,3]
And Generic<T>
requires T
to be a string, but, as shown, not all values of all keys of an array are strings.
You can fix the mapped type very simply by intersecting the array keys with number
, informing typescript that you only care about the number keys (which are the array indices):
export interface Generic<T extends string> {
}
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T & number]: Generic<T[P]> }
}
Upvotes: 1