iceblueorbitz
iceblueorbitz

Reputation: 1060

How to specify generics in an array if interfaces in TypeScript

I have a function that takes an array of specifications as an argument. The shape of these specifications are defined, but the values can be anything. However - I'd like to provide a generic to let TypeScript infer the types based on the specs I pass into the function. Here's a very basic example:

interface Spec<YourType> {
    value: YourType;
}

function doStuff<T>(specs: Spec<T>[]): T[] {
    return specs.map(spec => spec.value);
}

const mySpecs = [
    {value: 1},
    {value: 'two'},
    {value: [1, 2, 3]},
];

const values = doStuff(mySpecs);

I understand it can't really infer types here because they're all different, but can I somehow add them as types explicitly to the doStuff generic. Something like this pseudo-code:

function doStuff<T>(specs: <Spec<t> for t of T>): <t for t of T> {
    return specs.map(spec => spec.value);
}

const values = doStuff<[number, string, number[]]>(mySpecs);

Upvotes: 0

Views: 534

Answers (2)

jcalz
jcalz

Reputation: 329678

TypeScript 3.1 introduced support for using mapped tuple types, so if you have a type like [1,2,3] and apply a mapped type like {[K in keyof T]: Spec<T[K]>} to it, it will become [Spec<1>, Spec<2>, Spec<3>]. Therefore the signature for your function could be something like this:

declare function doStuff<T extends readonly any[]>(
  specs: { [K in keyof T]: Spec<T[K]> } | []
): T;

That's fairly close to your pseudo-code. The only differences of note is that we are saying that T has to be either an Array or ReadonlyArray (a ReadonlyArray is considered a wider type than Array), and the specs parameter's type has a union with an empty tuple type | [] in there to give the compiler a hint that it should infer T to be a tuple type if possible, instead of just an order-forgetting array. Here's what usages would look like:

const values = doStuff([
  { value: 1 },
  { value: 'two' },
  { value: [1, 2, 3] },
]);
// const values: [number, string, number[]]

If you want to do that in two separate lines with mySpecs you should probably tell the compiler not to forget that mySpecs is a tuple type, like this:

const mySpecs = [
  { value: 1 },
  { value: 'two' },
  { value: [1, 2, 3] },
] as const;
/* const mySpecs: readonly [
  { readonly value: 1; }, { readonly value: "two"; }, { readonly value: readonly [1, 2, 3];
}] */

const moreValues = doStuff(mySpecs);
// const moreValues: readonly [1, "two", readonly [1, 2, 3]]

Here I've used a TypeScript 3.4+ const assertion to keep the type of mySpecs narrow... maybe it's too narrow since everything becomes readonly and literal types. But that's up to you to work with.


This would be the end of the answer except that, unfortunately, the compiler cannot verify that the implementation of doStuff() conforms to the signature:

return specs.map(spec => spec.value); // error! not callable

The easiest way around this is to use some judicious type assertions to convince the compiler first that specs has a usable map method (by saying it's a Spec<any>[] instead of just T, and that the output is indeed a T:

function doStuff<T extends readonly any[]>(specs: { [K in keyof T]: Spec<T[K]> } | []): T {
  return (specs as Spec<any>[]).map(spec => spec.value) as readonly any[] as T;
}

That compiles now and should be close to what you're looking for. Okay, hope that helps; good luck!

Playground link to code

Upvotes: 1

syntagma
syntagma

Reputation: 24344

You will have to provide union type definition of YourType to make it work:

const mySpecs = [
    {value: 1},
    {value: 'two'},
    {value: [1, 2, 3]},
];

type YourType = number | string | number[];

interface Spec<YourType> {
    value: YourType;
}

function doStuff<T>(specs: Spec<T>[]): T[] {
    return specs.map(spec => spec.value);
}

const values = doStuff<YourType>(mySpecs);
console.log(values);

Upvotes: 0

Related Questions