As an example, say I have a simple function which maps a variadic number of things to an array of objects like { a: value }
.
const mapToMyInterface = (...things) => things.map((thing) => ({ a: thing}));
Typescript in not yet able to infer a strong type for the result of this function yet:
const mapToMyInterface = mapToInterface(1, 3, '2'); // inferred type{ a: any }[]
First, I define a type that describes an array mapped to observables:
type MapToMyInterface<T extends any[]> = {
[K in keyof T]: T[K] extends Array<infer U> ? { a: U } : { a: T[K] }
}
Now I update my function:
const mapToMyInterface = <T extends any[]>(...things: T): MapToMyInterface<T> => things.map((thing) => ({ a: thing}));
So far, Typescript is not happy. The return expression of the function is highlighted with the error "TS2322: Type '{ a: any; }[]' is not assignable to type 'MapToMyInterface'"
Obviously, the parameter thing
needs to be explicitly typed in the mapping function. But I don't know of a way to say "the nth type", which is what I need.
That is to say, neither marking thing
as a T[number]
or even doing the following works:
const mapToMyInterface = <T extends any[], K = keyof T>(...things: T): MapToMyInterface<T> => things.map((thing: T[K]) => of(thing));
Is is possible for this to work in Typescript?
EDIT after @jcalz's answer: For posterity, I wanted to post the original motivation for my question, and the solution I was able to get from @jcalz's answer.
I was trying to wrap an RxJs operator, withLatestFrom
, to lazily evaluate the observables passed into it (useful when you may be passing in an the result of a function that starts an ongoing subscription somewhere, like store.select
does in NgRx).
I was able to successfully assert the return value like so:
export const lazyWithLatestFrom = <T extends Observable<unknown>[], V>(o: () => [...T]) =>
concatMap(value => of(value).pipe(withLatestFrom(...o()))) as OperatorFunction<
V,
[V, ...{ [i in keyof T]: TypeOfObservable<T[i]> }]
>;
Let's say you have a generic function
wrap()
which takes a value of typeT
and returns a value of type{a: T}
, as in your example:If you just make a function which takes an array
things
and callsthings.map(wrap)
, you'll get a weakly typed function, as you noticed:This completely forgets about the individual types that went in and their order, and you just an array of
{a: any}
. It's true enough, but not very useful:Darn, the compiler didn't catch the fact that
2
refers to the third element of the array which is a wrappedDate
and not a wrappedstring
. I had to wait until runtime to see the problem.If you look at the standard TypeScript library's typing for
Array.prototype.map()
, you'll see why this happens:When you call
things.map(wrap)
, the compiler infers a singleU
type, which is unfortunately going to be{a: any}
, because ifthings
is of typeT extends any[]
, all the compiler knows about the elements ofthings
is that they are assignable toany
.There's really no good general typing you could give to
Array.prototype.map()
that will handle the case where thecallbackfn
argument does different things to different types of input. That would require higher kinded types like type constructors, which TypeScript doesn't currently support directly (see microsoft/TypeScript#1213 for a relevant feature request).But in the case where you have a specific generic type for your
callbackfn
, (e.g.,(x: T) => {a: T}
), you can manually describe the specific type transformation on a tuple or array using mapped array/tuple types.Here it is:
What we're doing here is just iterating over each (numeric) index
K
of theT
array, and taking theT[K]
element at that index and mapping it to{a: T[K] }
.Note that because the standard library's typing of
map()
does not anticipate this particular generic mapping function, you have to use a type assertion to have it type check. If you're only concerned about the compiler's inability to verify this without a type assertion, this is really about the best you can do without higher kinded types in TypeScript.You can test it out on the same example as before:
Now the compiler catches the mistake, and tells me that
Date
objects don't have atoUpperCase
method. Hooray!Your version of the mapping,
is a little weird because you're doing the mapping twice; unless you're passing in arrays of arrays, there's no reason to check
T[K]
for whether or not it's an array itself.T
is the array, andK
is the index of it. So I'd say just return{a: T[K]}
unless I'm missing something important.Playground link to code