Reputation: 1060
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
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!
Upvotes: 1
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