Joseph Garrone
Joseph Garrone

Reputation: 1762

TypeScript: Is it possible to map tuples in a typesafe way?

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

Upvotes: 1

Views: 151

Answers (2)

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:

function map<
  Elem,
  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!"]
  
function map<
  Elem,
  Tuple extends Elem[]
>(tuple: [...Tuple]): MapUtility<Tuple>
function map(tuple: unknown[]) {
  return tuple.map(callbackFn)
}

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

Playground

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

Upvotes: 1

Gergely Sipos
Gergely Sipos

Reputation: 81

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.

Upvotes: 1

Related Questions