0

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);

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

Comments

0

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);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.