Mapping tuple-typed value to different tuple-typed value without casts

No, this is not possible in TypeScript as of TS4.1. There are two issues I see; one might be overcome with a change to the TypeScript standard library typings for Array.prototype.map(), but the other would require a fairly large change to the type system to work in general and cannot currently be handled in a way that isn’t, in some sense, equivalent to a type assertion (what you’re calling a “cast”).


The current library typings for the map() method of an array is:

interface Array<T> {
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
}

The return type, U[] is an unordered array type corresponding to the output type of the callbackfn argument. It is not a tuple. There is an open GitHub issue, microsoft/TypeScript#29841 asking to change this so that when you call map() on a tuple, you get a tuple of the same length out. It is not currently implemented; but you can use declaration merging to test such a change out for yourself:

// declare global { // uncomment if in a module
interface Array<T> {
  map<This extends Array<T>, U>(this: This, fn: (v: T) => U): { [K in keyof This]: U }
}
interface ReadonlyArray<T> {
  map<This extends ReadonlyArray<T>, U>(
    this: This, fn: (v: T) => U): { [K in keyof This]: U }
}
// }

If I call convertValues() (with a change to the arr parameter as a variadic tuple type which gives the compiler a hint that it should interpret arr as a tuple if possible), you can see how it now knows how many elements are in the return value:

function convertValues<T extends any[]>(arr: [...SomeTypeList<T>]) {
  return arr.map(({ value }) => ({ otherValue: value }))
}

const ret = convertValues([{ value: 1 }, { value: 'cat' }, { value: true }])
ret[0]; ret[1]; ret[2]; // okay
ret[3] // error! Tuple type of length '3' has no element at index '3'.

Unfortunately, the types of each tuple element are not known:

ret[0].otherValue.toFixed(2); // error!
// -------------> ~~~~~~~
// Property 'toFixed' does not exist on type 'string | number | boolean'.

Each element is only known to the compiler to be {otherValue: string | number | boolean}. This isn’t incorrect, but also isn’t what you’re looking for.


So let’s step back and think about what you’d need map() to be like for this to do what you want. When you call arr.map(), you’re thinking of the callback as a generic function that turns an input of a generic type SomeType<V> to an output of type OtherType<V>, for any V. Otherwise there’s no chance of getting the compiler to notice the correlation between each element of the input tuple and the corrsponding element of the output tuple. You can indeed write that when you call map() by annotating the callback:

function convertValues<T extends any[]>(arr: [...SomeTypeList<T>]) {
  return arr.map(<V,>(
    { value }: SomeType<V>
  ): OtherType<V> => ({ otherValue: value }))
}

The problem is… how do you describe such generic callbacks in general which might do something else? I presume you don’t want to hardcode the typing for map() to care about callbacks that specifically turn SomeType<V> into OtherType<V>. You want to say “callbacks that turn F<X> into G<X> for any F and G“:

// not valid TypeScript, don't use it
interface Array<T> {
  map<A extends any[], F<?>, G<?>>(
    this: { [K in keyof A]: F<A[K]>},
    fn: <V>(v: F<V>) => G<V>
  ): { [K in keyof A]: G<A[K]> }
}

But there is no way to express this in TypeScript. F and G above are not generic types, but generic type functions or type constructors. And these do not exist in TypeScript. Generic type constructors would require introduction of so-called higher kinded types as can be found in some more functional-programming-heavy languages like Haskell and Scala. There is a longstanding open feature request, microsoft/TypeScript#1213 asking for this, but who knows if it will ever be implemented. It would be quite a bit of work, so I’m not holding my breath (but would love to see it!). and there are some possible ways to simulate higher kinded types in TypeScript (you can read that GitHub issue for more), but nothing I’d want to recommend for your use case.

So we’re stuck. There’s currently no way to write map()‘s typing to get the compiler to verify that convertValues()‘s implementation conforms to the type you claim to return.


And when the compiler can’t verify that the type of something is what you claim it is, and you’re confident that your claim is nevertheless correct, you pretty much need to do something like a type assertion, as you’ve already done. So I’d recommend you keep doing it the way you’ve shown here, and revisit if higher kinded types ever show up in TypeScript.

Playground link to code

Leave a Comment