Skip to content

Instantly share code, notes, and snippets.

@jcalz
Last active March 1, 2024 03:33
Show Gist options
  • Select an option

  • Save jcalz/381562d282ebaa9b41217d1b31e2c211 to your computer and use it in GitHub Desktop.

Select an option

Save jcalz/381562d282ebaa9b41217d1b31e2c211 to your computer and use it in GitHub Desktop.
TypeScript tuple inference

You can use the tuple() function in tuple.ts to infer tuple types in TypeScript and cut down on the need to repeat yourself. Without tuple(), declaring a constant of a tuple type looks like this:

const daysOfTheWeek: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] = 
  ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];

You can't do this:

const daysOfTheWeek = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; 
  // right value, wrong type

because TypeScript would infer the type of daysOfTheWeek to be a plain array of string values.

And you can't do this:

const daysOfTheWeek: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; 
  // right type, wrong value

because then the variable daysOfTheWeek would not be defined at runtime.

Enter tuple():

import { tuple } from './tuple'; // or wherever you put your libraries
const daysOfTheWeek = 
  tuple("sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"); // right type, right value

If you inspect the type of daysOfTheWeek, it will be the desired tuple type ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"], and has the correct value at runtime.


Note that the inferred tuple type will be as narrow as it can, interpreting any string, number, or boolean literal element as the corresponding literal type:

const narrow = tuple("a", 1, true); // inferred as type ["a", 1, true]

If you want to widen any of them, you can do type assertions:

const wider = tuple("a" as string, 1 as 0 | 1 | 2, true as boolean | undefined) 
// inferred as [string, 0 | 1 | 2, boolean | undefined]

Also note that the library only supports tuples of length up to twelve; you can add more overloads at the top if you need them.

export type Lit = string | number | boolean | undefined | null | void | {};
// infers a tuple type for up to twelve values (add more here if you need them)
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit, K extends Lit, L extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L): [A, B, C, D, E, F, G, H, I, J, K, L];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit, K extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K): [A, B, C, D, E, F, G, H, I, J, K];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J): [A, B, C, D, E, F, G, H, I, J];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I): [A, B, C, D, E, F, G, H, I];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): [A, B, C, D, E, F, G, H];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G): [A, B, C, D, E, F, G];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F): [A, B, C, D, E, F];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit>(a: A, b: B, c: C, d: D, e: E): [A, B, C, D, E];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit>(a: A, b: B, c: C, d: D): [A, B, C, D];
export function tuple<A extends Lit, B extends Lit, C extends Lit>(a: A, b: B, c: C): [A, B, C];
export function tuple<A extends Lit, B extends Lit>(a: A, b: B): [A, B];
export function tuple<A extends Lit>(a: A): [A];
export function tuple(...args: any[]): any[] {
return args;
}
@seanf

seanf commented Mar 16, 2018

Copy link
Copy Markdown

I have a slightly modified version which makes the tuple elements readonly: https://gist.github.com/seanf/b056a40a7256efee10f53c82e490b5b3

Honestly, it doesn't make much difference if you're making a tuple of literals, because the compiler won't let you assign a different value to the tuple, but it bothered me slightly...

I got the syntax I needed from, who else, @jcalz: microsoft/TypeScript#17563 (comment) Cheers mate!

@jakewtaylor

Copy link
Copy Markdown

Another option for any amount of arguments:

function tuple <T extends [void] | {}> (val: T): T {
    return val;
}

tuple([1, '2', true]);
// -> [number, string, boolean];

tuple([1 as 1, '2' as '2', true as true]);
// -> [1, '2', true]

@jcalz

jcalz commented Jul 3, 2018

Copy link
Copy Markdown
Author

Yes, it looks like Microsoft/TypeScript#24897 will remove the need for tuple.ts.

I would probably use

export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

which works like the original for any number of elements...

@trusktr

trusktr commented Nov 29, 2018

Copy link
Copy Markdown

@jcalz It is sort of weird, even the following works for any types:

export type Lit = symbol | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

const arr = tuple(1, 2, 3, 'hello', new Date, false)
// typeof arr is [1, 2, 3, 'hello', Date, false]

Playground link

Hover over arr and you see it works. Seems like the minimum requirement for the Lit type is to make a union with {} and any other type, then it'll work.

For example, this works too:

export type Lit = boolean | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

const arr = tuple(1, 2, 3, 'hello', new Date, false)
// typeof arr is still [1, 2, 3, 'hello', Date, false]

Playground

@bcherny

bcherny commented Dec 17, 2018

Copy link
Copy Markdown

You don't even need Lit!

export const tuple = <T extends unknown[]>(...args: T) => args;
const arr = tuple(1, 2, 3, 'hello', new Date, false)
// [1, 2, 3, 'hello', Date, false]

@bencinity

Copy link
Copy Markdown

@bcherny are you sure?

When using unknown in the playground it seems to come back as [number, number, number, string, Date, boolean]

@bencinity

bencinity commented Feb 7, 2019

Copy link
Copy Markdown

Btw, if anyone's interested I created a node module typed-tuple based on this code a while back. I so far haven't upgraded to the new shorter code for backwards compatibility with older TS versions.

@jcalz

jcalz commented Feb 10, 2019

Copy link
Copy Markdown
Author

We're getting very close to not needing this at all... In TypeScript 3.4, which should be released in March 2019 it will be possible to tell the compiler to infer the type of a tuple of literals as a tuple of literals by using the as const syntax. It should look like this:

const list = ['a', 'b', 'c'] as const;  // const list: readonly ['a', 'b', 'c']

@laurencestokes

Copy link
Copy Markdown

@jcalz

using

export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

and invoking tuple('string1', 'string2') gives me error TS2370: A rest parameter must be of an array type (presumably because I'm passing in 4 seperate string arguments, not an array it can destructure?)

@trusktr

trusktr commented May 19, 2019

Copy link
Copy Markdown

as const is nice!

@laurencestokes I can't reproduce your problem, it works in playground.

@nathanredblur

Copy link
Copy Markdown

as const was perfect. some link to the documentation to know more?

@karataev

karataev commented Apr 8, 2020

Copy link
Copy Markdown

as const was perfect. some link to the documentation to know more?

This feature was added in TS 3.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment