I am trying to create a function that extends the objects in an array without losing literals from the generics. Is there a way to do this?
type Identified<Id extends string = string> = { id: Id };
type Extended<Id extends string> = Identified<Id> & { test: "test" };
let one: Identified<"one"> = {id:"one"};
let two: Identified<"two"> = {id:"two"};
let three: Identified<"three"> = {id:"three"};
function extendList<A extends Identified>(arr: A[]) {
return arr.map((item) => ({ ...item, test: "test" }));
}
let extendedList = extendList([one,two,three]); // literal is lost
let oneExtended = extendedList[0]; // this should be {id:"one", test:"test"} (not {id:string...})
CodePudding user response:
One approach is
function extendList<T extends string[]>(
arr: [...{ [I in keyof T]: Identified<T[I]> }]
) {
return arr.map((item) => ({ ...item, test: "test" })) as
{ [I in keyof T]: Extended<T[I]> };
}
The idea is to make extendList()
generic in T
, the tuple of the string literal types of the id
properties of the elements of the arr
parameter. So if you call extendList([one, two, three])
, T
should be ["one", "two", "three"]
.
Then the type of the input arr
is a mapped tuple that turns each element T[I]
(the I
th element of the T
tuple) into Identified<T[I]>
, while the output is a mapped tuple that turns each element T[I]
into Extended<T[I]>
.
Note that the type of arr
isn't just the mapped type { [I in keyof T]: Identified<T[I]> }
, but has been wrapped in a variadic tuple of the form [...{ [I in keyof T]: Identified<T[I]> }]
; this is just to give the compiler a hint that it should tend to infer tuple types over unordered arrays. Otherwise extendList([one, two, three])
might have T
be inferred as ("one" | "two" | "three")[]
, which is not what you want.
Also note that I needed to use a type assertion to tell the compiler that arr.map(item => ({...item, test: "test"}))
is of the desired output type. The compiler can't infer that automatically, nor can it verify it. It's essentially beyond the compiler's abilities to "see" that arr.map(...)
does what you say it does. See this answer to a similar question for more information about this limitation.
Anyway, let's make sure it behaves as you want:
let one: Identified<"one"> = { id: "one" };
let two: Identified<"two"> = { id: "two" };
let three: Identified<"three"> = { id: "three" };
let extendedList = extendList([one, two, three]);
// let extendedList: [Extended<"one">, Extended<"two">, Extended<"three">]
let oneExtended = extendedList[0];
// let oneExtended: Extended<"one">
Looks good!
CodePudding user response:
Try this
type Identified<Id extends string = string> = { id: Id };
type Extended<Id extends string> = Identified<Id> & { test: "test" };
const one: Identified<"one"> = {id:"one"};
const two: Identified<"two"> = {id:"two"};
const three: Identified<"three"> = {id:"three"};
function extendList<T extends string>(arr: Identified<T>[]): Extended<T>[] {
return arr.map((item) => ({ ...item, test: "test" }));
}
const extendedList = extendList([one,two,three]);
const oneExtended = extendedList[0];