我正在使用 Typescript 创建一个将棋游戏板。将棋盘有 9 个等级和档。
我想将 9x9 多维数组断言为一种类型,以确保数组的大小和内容。
目前我正在用这种方式创建 9x9 板类型:
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;
}
问题:是否有一种不那么繁琐、更通用的方法来定义我称之为
Board9x9<P>
的元组类型?
更新:
使用 递归条件类型(在 TypeScript 4.1.0 中添加)可以:
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>>;
原答案:
Typescript 3 引入了元组类型中的剩余元素
元组类型的最后一个元素可以是 ...X 形式的剩余元素,其中 X 是数组类型
为了限制元组的长度,我们可以使用与
{ length: N }
的交集
type Tuple<TItem, TLength extends number> = [TItem, ...TItem[]] & { length: TLength };
type Tuple9<T> = Tuple<T, 9>;
type Board9x9<P> = Tuple9<Tuple9<P>>;
这在初始化
Tuple
类型的变量时起作用:
const t: Tuple<number, 1> = [1, 1] // error: 'length' incompatible.
这里需要注意,如果您尝试访问索引处超出元组范围的非元素,打字稿不会警告您:
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'
有一个建议添加一种通用方法来指定元组类型的长度。
一个快速简化方法是创建一个
Tuple9
类型,可用于创建矩阵的第一级和第二级:
type Tuple9<T> = [T, T, T, T, T, T, T, T, T]
type Board9x9<P> = Tuple9<Tuple9<P>>
您可以借助元组类型别名制作任意 NxN 板:
type Tuple<T, N extends number, A extends any[] = []> = A extends { length: N } ? A : Tuple<T, N, [...A, T]>;
所以在你的情况下你会做类似的事情:
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>;
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']
我是这样分三步完成的
type Tuple<V, N extends number, T extends V[] = []> =
N extends T['length'] ? T : Tuple<V, N, [...T, V]>;
用途:
type String3 = Tuple<string, 3>; // [string, string, string]
type String3Number = [...String3, number]; // [string, string, string, number]
这可以一行完成。
type Tuple<T, N, R extends T[] = []> = R['length'] extends N ? R : Tuple<T, N, [...R, T]>;
用途:
Tuple<MyType, 100>;
对常见用例(如本问题)的思考,其中您尝试创建的类型不应具有会改变底层数组的不安全数组方法(如
push
、pop
等):
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
不要使用元组,请考虑使用仅限某些索引的索引签名:
// 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>'.
扩展已接受的解决方案https://stackoverflow.com/a/52490977/1712683
您可以通过从继承自 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>
基于 mstephen19 和 Joey Kilpatrick's 的答案,您可以使用
readonly
前缀确保元组不会发生变化,而无需选择或省略属性:
type Tuple<T, N, R extends readonly T[] = []> = R['length'] extends N ? R : Tuple<T, N, readonly [...R, T]>;
readonly
关键字确保元组一旦创建就无法修改。您仍然可以使用无副作用的方法,例如 slice
。但是,改变数组的方法(例如 push
、pop
或 splice
)在只读元组上不可用。