user4704976
user4704976

Reputation:

Possible to use Array.prototype.map() on tuple in TypeScript while preserving tuple length in return type?

I was hoping that if I use the built-in map function on a tuple of length N in TypeScript then the return type would also be a tuple of length N (perhaps with different type for the elements depending on the function being passed to map). Instead, the return type is just a standard, variable-length array of whatever type the callback function returns. The tuple's length is lost. I wrote a custom function that does what I want, but I'm wondering if there's a better way that is eluding me. I'm trying to improve my understanding of TypeScript. I included a TypeScript Playground link below the code (with the same code). Thanks for your help!

const nums = [1, 2, 3] as const;

// result1 type is string[]
const result1 = nums.map((num) => num.toString());

// so this won't compile
const result2: [string, string, string] = nums.map((num) => num.toString());

// a type assertion would work, but I'd rather not use one...
const result3 = nums.map((num) => num.toString()) as [string, string, string];

// ...because this also compiles yet the type of result4 doesn't match its value
const result4 = nums.map((num) => num.toString()) as [string, boolean, number, symbol, string, number];

// result5 type is [string, string, string]
const result5 = map(nums, (num) => num.toString());

// custom map function that yields the correct return type when used on a tuple
function map<A extends readonly [...any[]], B>(values: A, func: (value: A[number]) => B): { [K in keyof A]: B } {
    return values.map(func) as unknown as { [K in keyof A]: B };
}

See on: TypeScript Playground

Upvotes: 4

Views: 1110

Answers (1)

Please try next:


type A = readonly [1, 2, 3]

const nums: A = [1, 2, 3];
const toStr = <T extends number>(num: T) => num.toString() as `${T}`

const result2 = nums.map(toStr); // ("3" | "1" | "2")[]

I believe this is not the best solution, because you still have ('3'|'2'|'1')[] and not ['1','2','3'], but this is some kind of step forward

I will happy to see other solutions here. Very interesting problem )

Works only with T.S 4.1

UPDATE

There is a possibility to create type what you want with some help of overloading and variadic tuple types

type Json =
    | string
    | number
    | boolean
    | { [prop: string]: Json }
    | Array<Json>

type CustomMap<T extends ReadonlyArray<Json>> = {
    [Index in keyof T]: { elem: T[Index] }
}

function map<Elem extends Json, List extends Elem[], Return>(list: [...List], callback: (value: Elem) => Return): CustomMap<List>
function map<Elem extends Json>(list: Elem[], callback: (elem: Elem) => unknown) {
    return list.map(callback)
}

// const result2: [{
//     elem: 1;
// }, {
//     elem: 2;
// }, {
//     elem: 3;
// }]
const result2 = map([1, 2, 3], elem => ({ elem }));

Playground

Upvotes: 2

Related Questions