Home > Back-end >  TypeScript: Is it possible to map tuples in a typesafe way?
TypeScript: Is it possible to map tuples in a typesafe way?

Time:12-15

If I write this:

declare function callbackFn<T>(entry: T): T extends string ? "string !" : "not string!";

const output= ([ "foo", 42 ] as const).map(callbackFn);

the type of output is ("string !" | "not string!")[] is there any way to get it as readonly [ "string !", "not string!" ]?

Thank you

CodePudding user response:

It is possible to achieve with help of function-overloads.

First of all, lets define callbackFn:


type CallbackFn<T> = T extends string ? "string !" : "not string!";

function callbackFn<T>(entry: T): CallbackFn<T>
function callbackFn<T>(entry: T) {
  return typeof entry === 'string' ? "string !" : "not string!"
}

I have overloaded callbackFn and used CallbackFn type utility to infer return type.

Now, we need type utility for our mapping. Consider this:


type MapUtility<T extends ReadonlyArray<unknown>> = {
  [Prop in keyof T]: CallbackFn<T[Prop]>
}

type Test1 = MapUtility<["foo", 42]> // ["string!", "not string!"]

Since all utils are set we can write our main function. Keep in mind, that it is not necessary to use as const to infer literal argument. You can use variadic-tuple-types:

type Json =
  | null
  | string
  | number
  | boolean
  | Array<JSON>
  | {
    [prop: string]: Json
  }
  
function map<
  Elem extends Json,
  Tuple extends Elem[]
>(tuple: [...Tuple]): MapUtility<Tuple>
function map(tuple: unknown[]) {
  return tuple.map(callbackFn)
}

// ["string !", "not string!"]
const output = map(["foo", 42])

Whole example:



type CallbackFn<T> = T extends string ? "string !" : "not string!";

function callbackFn<T>(entry: T): CallbackFn<T>
function callbackFn<T>(entry: T) {
  return typeof entry === 'string' ? "string !" : "not string!"
}


type MapUtility<T extends ReadonlyArray<unknown>> = {
  [Prop in keyof T]: CallbackFn<T[Prop]>
}

type Test1 = MapUtility<["foo", 42]> // ["string!", "not string!"]

type Json =
  | null
  | string
  | number
  | boolean
  | Array<JSON>
  | {
    [prop: string]: Json
  }
  
function map<
  Elem extends Json,
  Tuple extends Elem[]
>(tuple: [...Tuple]): MapUtility<Tuple>
function map(tuple: unknown[]) {
  return tuple.map(callbackFn)
}

// ["string !", "not string!"]
const output = map(["foo", 42])

Playground

I have used Json type because I assume that any other type is allowed in the tuple, not only string and number.

Please keep in mind that typescript does not preserve tuple length after map. See this answer.

This answer is related

P.S. If you are interested in function argument inference in TypeScript you can check my article

CodePudding user response:

If you are working with tuple types, I believe you have to avoid .map, try this instead:

    const mapTuple = (tuple: [unknown, unknown]) => [callbackFn(tuple[0], callbackFn(tuple[1])] as const;

This way you still keep the tuple type as a return type.

  • Related