64

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;
}

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

ZachB
  • 13,051
  • 4
  • 61
  • 89
Lucas
  • 1,149
  • 1
  • 9
  • 23

8 Answers8

97

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.

Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
  • 1
    why `N extends N` in your example? Isn't it always true? – trias Oct 16 '20 at 12:26
  • 5
    @trias you're right, this is not related to the question, but this triggers distribution over union and allows things like: `type VarLength = Tuple;` which results in `[] | [number, number] | [number, number, number, number]` – Aleksey L. Oct 16 '20 at 13:45
  • If union support is not needed, it could be just `type Tuple = R['length'] extends N ? R : Tuple;` – Aleksey L. Mar 30 '21 at 06:07
  • Check out https://stackoverflow.com/a/68216613/57611 which uses a similar recursive `Tuple` definition, but offers a solution to the invalid index problem. As one might expect, though, it has its own issues (can't extend the resulting class easily). – ErikE Jul 04 '21 at 04:09
  • @ErikE The latest solution checks indexes properly – Aleksey L. Jul 04 '21 at 07:20
  • 1
    @AlekseyL. Ah... I must have missed something, Was reading the part saying indexes were not checked properly. – ErikE Jul 05 '21 at 03:16
  • @AlekseyL. Thanks for this! It's pretty neat :) Do you have any idea whether it could be possible at all to make this work? https://shorturl.at/krBP7 – ccjmne Sep 06 '21 at 16:04
  • @ccjmne sorry, was away for a while. The link is no longer valid, could you recreate if still relevant? – Aleksey L. Sep 19 '21 at 07:05
  • @AlekseyL. Woops, sorry I missed your reply as well... I'm pretty sure what I want is legitimately impossible right now, but if you'd wanna take a look at it, I'd be very grateful! Here's what I'd like to do with my Tuples :) https://shorturl.at/kyCZ8 – ccjmne Sep 23 '21 at 16:14
  • @ccjmne the link is dead again – Aleksey L. Sep 29 '21 at 06:09
  • 1
    @AlekseyL. Ah! This is embarrassing I'll message in you in a chat room :) – ccjmne Sep 29 '21 at 19:22
  • This is amazing! With this Tuple type, you can create type guards for `Array.prototype.length` checks! – derpedy-doo Sep 01 '22 at 15:51
  • Is there anyway we could make the length part also variable size. what I mean is can I create a ```typescript const a: number = 5; const tuple: Tuple = [1,2,3,4,5]; ``` – AK-35 Jan 04 '23 at 01:21
  • @AhmetK yes it is possible: `const a = 5; const tuple: Tuple = [1,2,3,4,5];` – Aleksey L. Jan 05 '23 at 08:01
25

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>>
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 2
    That is pretty darn slick, but still could get ugly if I wanted to create a `Tuple36` for a Taikyoku shogi board. xD. Still, your strategy is a significant code shrinkage compared to my strategy. Thank you! – Lucas Sep 25 '18 at 01:35
  • 2
    It is true that this method can be more verbose than Aleksey's method. However, when I use Aleksey's method, the TS compiler lets me access indices that are out of bounds whereas the method in this answer prevents it. E.g. if I declare `const q: Board9x9 = ...` then with Aleksey's method `q[100][100]` compiles fine but with the code in this answer the compiler *correctly reports* a problem with the indices. – Louis Mar 21 '19 at 18:21
  • Just adding this here for completeness, you can now specify a tuple of length N as follows: `type Tuple = A extends { length: N } ? A : Tuple;` – sstur Jan 27 '21 at 07:42
  • cool, sometimes, simple is the best. – shtse8 Jul 28 '22 at 14:23
4
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

Deadalus _
  • 66
  • 4
4

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>;
sstur
  • 1,769
  • 17
  • 22
2

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]
Salathiel Genese
  • 1,639
  • 2
  • 21
  • 37
2

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>;
mstephen19
  • 1,733
  • 1
  • 5
  • 20
0

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>'.
Joey Kilpatrick
  • 1,394
  • 8
  • 20
0

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>
Harry
  • 1,572
  • 2
  • 17
  • 31