This works:
class Dog {
speak() {
return "woof" as const;
}
}
type ExtractSpeak<T extends [...Dog[]]> = { [P in keyof T]: T[P] extends Dog ? ReturnType<T[P]["speak"]> : never };
// Type of Example is ["woof", "woof"]
type Example = ExtractSpeak<[Dog, Dog]>;
But, why doesn't it work without the conditional type?
// Error: Type '"speak"' cannot be used to index type 'T[P]'. (2536)
type ExtractSpeak<T extends [...Dog[]]> = { [P in keyof T]: ReturnType<T[P]["speak"]> };
When would T[P]
refer to anything other than a type having a speak
method which returns "woof"
?
It's a bug in TypeScript; see microsoft/TypeScript#27995. When you make a mapped type over a tuple/array the type that comes out will also be a tuple/array; that is, you're only really mapping over the numeric indices of the array. But inside the definition of the mapped type, the compiler doesn't pay attention to this. Instead, it sees K in keyof T
(where T
is an array type) as possibly iterating K
over every possible key, like this:
type DogArrayKeys = keyof [Dog, Dog];
// number | "0" | "1" | "length" | "toString" | "toLocaleString" | "pop" |
// "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" |
// "splice" | "unshift" | "indexOf" | "lastIndexOf" |
// ... 14 more ... | "includes"
And since, for example, [Dog, Dog]["length"]
is of type 2
and not Dog
, the compiler won't let you treat T[K]
as Dog
. Blecch.
Anyway, it's a bug and it would be nice if it were fixed. I suppose you can go to ms/TS#27995 and give it a 👍. But realistically it is probably not high on any priority list (it's on the backlog for now). The standard workaround is just to use a conditional type like Extract<T[K], Dog>
and move on, which is basically what you have above.