Reputation: 1270
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
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
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
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
Reputation: 1909
Here's how I did it in three steps
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
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>>;
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
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
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
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
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