Lucas
Lucas

Reputation: 1270

Can I define an n-length tuple type?

I am creating a shogi game board using TypeScript. A shogi board has 9 ranks and files.

I'd like to assert a 9x9 multidimensional array as a type to ensure both the size and contents of the array.

Currently I am creating my 9x9 board type this way:

type Board9x9<P> = [
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P],
  [P, P, P, P, P, P, P, P, P]
];

interface IShogiBoardInternalState {
    board: Board9x9<IShogiPiece>;
    playerName: string;
    isYourTurn: boolean;
}

Is there a less tedious, more generic way to define this tuple type which I have called Board9x9<P>?

Upvotes: 74

Views: 33729

Answers (9)

user13581317
user13581317

Reputation:

Building upon mstephen19 and Joey Kilpatrick's answers, you can ensure the tuple won't be mutated without needing to pick or omit properties by using the readonly prefix:

type Tuple<T, N, R extends readonly T[] = []> = R['length'] extends N ? R : Tuple<T, N, readonly [...R, T]>;

const tuple: Tuple<string, 2> = ["foo", "bar"];
tuple.pop // Property 'pop' does not exist on type...
tuple.slice(1) // Ok

readonly will remove methods that mutate the tuple from the type, such as pop. You'll still have access to side-effect-free methods such as slice.

Upvotes: 0

Harry
Harry

Reputation: 1742

Extending the accepted solution https://stackoverflow.com/a/52490977/1712683

You can handle the case to restrict people from updating the array by removing these types from the default types that are inherited from Array.prototype

type Exclude = 'push' | 'pop' | 'splice'
type Tuple<T, L extends number> = Omit<T[], Exclude> & {length: L}

type Board9x9<P> = Tuple<Tuple<P, 9>, 9>

Upvotes: 0

mstephen19
mstephen19

Reputation: 1926

This can be done in one line.

type Tuple<T, N, R extends T[] = []> = R['length'] extends N ? R : Tuple<T, N, [...R, T]>;

Usage:

Tuple<MyType, 100>;

Upvotes: 5

Salathiel Genese
Salathiel Genese

Reputation: 1909

Here's how I did it in three steps

  • Initialize tuple with an empty array tuple of the value type
  • If the length of the tuple equals the desired length, return the tuple
  • Otherwise, recursively add value type to the tuple
type Tuple<V, N extends number, T extends V[] = []> =
    N extends T['length'] ? T : Tuple<V, N, [...T, V]>;

Usage:

type String3 = Tuple<string, 3>; // [string, string, string]
type String3Number = [...String3, number]; // [string, string, string, number]

Upvotes: 4

Aleksey L.
Aleksey L.

Reputation: 37918

Update:

With Recursive conditional types (added in TypeScript 4.1.0) it is possible to:

type Tuple<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;

type Tuple9<T> = Tuple<T, 9>;
type Board9x9<P> = Tuple9<Tuple9<P>>;

Playground



Original answer:

Typescript 3 introduces rest elements in tuple types

The last element of a tuple type can be a rest element of the form ...X, where X is an array type

To restrict the length of a tuple we can use intersection with { length: N }

type Tuple<TItem, TLength extends number> = [TItem, ...TItem[]] & { length: TLength };

type Tuple9<T> = Tuple<T, 9>;
type Board9x9<P> = Tuple9<Tuple9<P>>;

This works when variable of Tuple type is being initialized:

const t: Tuple<number, 1> = [1, 1] // error: 'length' incompatible.

A caveat here, typescript won't warn you if you'll try to access non element at index out of tuple range:

declare const customTuple: Tuple<number, 1>;
customTuple[10] // no error here unfortunately

declare const builtinTuple: [number];
builtinTuple[10] // error: has no element at index '10'

There's a suggestion to add a generic way to specify length of a tuple type.

Upvotes: 119

Joey Kilpatrick
Joey Kilpatrick

Reputation: 1602

A thought for the common use case (like in this question) where the type you are trying to create should not have unsafe array methods that will mutate the underlying array (like push, pop, etc.):

const board: Tuple<string, 4> = ["a", "b", "c", "d"];
board.pop()
const fourthElement: string = board[3]; // No TS error
fourthElement.toUpperCase() // Errors when run, but no TS error

Instead of using a tuple, consider using an index signature restricted to only certain indices:

// type BoardIndicies = 0 | 3 | 1 | 2
type BoardIndicies = Partial<Tuple<never, 3>>['length']

const board: Record<BoardIndicies, string> = ["a", "b", "c", "d"];
board.pop() // ERROR: Property 'pop' does not exist on type 'Record<0 | 3 | 1 | 2, string>'.

Upvotes: 0

sstur
sstur

Reputation: 1799

You can make an arbitrary NxN board with the help of a Tuple type alias:

type Tuple<T, N extends number, A extends any[] = []> = A extends { length: N } ? A : Tuple<T, N, [...A, T]>;

So in your case you'd do something like:

type Tuple<T, N extends number, A extends any[] = []> = A extends { length: N } ? A : Tuple<T, N, [...A, T]>;

type Board9x9<P> = Tuple<Tuple<P, 9>, 9>;

Upvotes: 7

Deadalus _
Deadalus _

Reputation: 76

type PushFront<TailT extends any[], FrontT> = (
  ((front : FrontT, ...rest : TailT) => any) extends ((...tuple : infer TupleT) => any) ?
  TupleT :
  never
)

type Tuple<ElementT, LengthT extends number, OutputT extends any[] = []> = {
  0 : OutputT,
  1 : Tuple<ElementT, LengthT, PushFront<OutputT, ElementT>>
}[
  OutputT["length"] extends LengthT ?
  0 :
  1
]

//type t3 = [string, string, string]
type t3 = Tuple<string, 3>
//type length = 0 | 3 | 1 | 2
type length = Partial<Tuple<any, 3>>['length']

Add a generic way to specify length of a tuple type #issuecomment-513116547

Upvotes: 4

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249556

One quick simplification would be to create a Tuple9 type, that can be used to create the first level as well as the second level of the matrix:

type Tuple9<T> = [T, T, T, T, T, T, T, T, T]
type Board9x9<P> = Tuple9<Tuple9<P>>

Upvotes: 28

Related Questions