Home > OS >  How to extend generic tuple in TypeScript
How to extend generic tuple in TypeScript

Time:07-26

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...})

Playground Link

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 Ith 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!

Playground link to code

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];
  • Related