I have an array like this:
const arr = [{id: 1, color: "blue"}, {id: 4, color: "red"}] as const
and I want to create a type from it that looks like this:
type Colors = ["blue", "red"]
I've tried something like this:
type ArrTransform<T extends Readonly<{id: number, color: string}[]>> = {
[K in Exclude<keyof T, (symbol | string)>]: T[K]["color"]
}
But it never seems to get the order of the elements or the length of the outputted array type correctly.
Is this something that can be done in Typescript?
CodePudding user response:
Mapped types on tuples are also tuples automatically. But there are a number of "gotchas" with this support, and it looks like you might have gotten caught in several of them.
Tuple-to-tuple mapping only works for homomorphic mapped types over a generic type. So type ArrTransform<T> = {[I in keyof T]: ...}
will work correctly, since in keyof T
makes the transform homomorphic, and since T
is a generic type parameter.
As soon as you change it to {[I in Exclude<keyof T, symbol | string>]: ...}
, presumably in an attempt to filter for numeric indices, you make the mapping no longer homomorphic, and things break. Even worse, for tuple types, the relevant "numeric" indices are actually string literal types like "0"
, "1"
, "2"
, etc., and not number literal types; so your filter would throw away the indices you care about.
We will therefore leave it like {[I in keyof T]: ...}
.
If {[I in keyof T]: ...}
turns tuples into tuples without having to try to explicitly mess with keyof T
on the left hand side, surely that means I
will only be observed to iterate over the numeric-like indices of T
, right? And so T[I]
will definitely be assignable to { id: number, color: string }
, right? Wrong.
Frustratingly, the compiler seems to take the view inside the body of the mapped type that I
might be "push"
or "pop"
or "length"
, and therefore even though T
is constrained to { id: number, color: string}
, T[I]
could end up being all kinds of things, most of which are function types. See microsoft/TypeScript#27995 for more information.
So we can't just write this:
type SadArrTransform<T extends Readonly<{ id: number, color: string }[]>> = {
[I in keyof T]: T[I]['color'] // error
}
as the compiler assumes that maybe T[I]
will not have a known "color"
property. Instead you need to write something that means "if T[I]
has a "color"
property, then the type of that property, otherwise... oh, I don't care... how about never
"? That looks like this:
type ArrTransform<T extends Readonly<{ id: number, color: string }[]>> = {
[I in keyof T]: T[I] extends { color: infer V } ? V : never
}
And this version now works how you want:
type X = ArrTransform<typeof arr>;
// type X = readonly ["blue", "red"]