1

I am trying to create a function that decorates objects in an array and returns them. The returned type for each item must keep the 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

bristweb
  • 948
  • 14
  • 14
  • 1
    You can certainly create the function and give it typings, but you won't be able to get the compiler to *infer* these typings nor *verify* that the implementation is correct. It looks like [this](https://tsplay.dev/w6PRGm); does that fully address your question? If so I could write up an answer; if not, what am I missing? – jcalz Jul 26 '22 at 01:40
  • that works perfectly! if you post it, I'll mark as answer! – bristweb Jul 26 '22 at 01:43

3 Answers3

1

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • thank you for the thorough answer. also very helpful for the links to mapped tuple and variadic tuple since those aren't really covered in the handbook https://www.typescriptlang.org/docs/handbook/2/keyof-types.html – bristweb Jul 26 '22 at 03:45
0

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];
Aron
  • 15,464
  • 3
  • 31
  • 64
0

In case anyone also needs to derive a tuple type from a generic tuple see the ExtensibleList below:

type Identified<Id extends string = string> = { id: Id };
type ExtensibleList<List, Extension> = {
  [I in keyof List]: List[I] & Extension;
};

let one: Identified<"one"> = {id:"one"};
let two: Identified<"two"> = {id:"two"};
let three: Identified<"three"> = {id:"three"};
const list = [one,two,three] as const;

type ExtendedIdList = ExtensibleList<typeof list,{test:"test"}>

let extendedList:ExtendedIdList = [{id:"one", test:"test"}, {id:"two", test:"test"}, {id:"three",test:"test"}];

Playground Link

this is because an Arrays are objects with numeric keys and mapped tuples were added in TS 3.1+

bristweb
  • 948
  • 14
  • 14