如何在 TypeScript 中声明固定长度的数组?
当你像这样为数组进行类型声明时:
position: Array<number>;
它将允许你创建一个任意长度的数组。然而,如果你想要一个包含特定长度数字的数组,例如 x、y、z 组件的长度为 3。你能为固定长度数组创建一种类型吗,就像这样?
position: Array<3>
答案#1
JavaScript 数组有一个接受数组长度的构造函数:
let arr = new Array<number>(3);
console.log(arr); // [undefined × 3]
然而,这只是初始大小,对更改它没有任何限制:
arr.push(5);
console.log(arr); // [undefined × 3, 5]
TypeScript 有元组类型,它允许你定义具有特定长度和类型的数组:
let arr: [number, number, number];
arr = [1, 2, 3]; // ok
arr = [1, 2]; // Type '[number, number]' is not assignable to type '[number, number, number]'
arr = [1, 2, "3"]; // Type '[number, number, string]' is not assignable to type '[number, number, number]'
答案#2
元组方法:
该解决方案提供了基于元组的严格 FixedLengthArray(即 SealedArray)类型签名。
语法示例:
// Array containing 3 strings
let foo : FixedLengthArray<[string, string, string]>
这是最安全的方法,因为它可以防止访问超出边界的索引。
实施:
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type FixedLengthArray<T extends any[]> =
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }
测试:
var myFixedLengthArray: FixedLengthArray< [string, string, string]>
// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ] // ✅ OK
myFixedLengthArray = [ 'a', 'b', 123 ] // ✅ TYPE ERROR
myFixedLengthArray = [ 'a' ] // ✅ LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ] // ✅ LENGTH ERROR
// Index assignment tests
myFixedLengthArray[1] = 'foo' // ✅ OK
myFixedLengthArray[1000] = 'foo' // ✅ INVALID INDEX ERROR
// Methods that mutate array length
myFixedLengthArray.push('foo') // ✅ MISSING METHOD ERROR
myFixedLengthArray.pop() // ✅ MISSING METHOD ERROR
// Direct length manipulation
myFixedLengthArray.length = 123 // ✅ READ-ONLY ERROR
// Destructuring
var [ a ] = myFixedLengthArray // ✅ OK
var [ a, b ] = myFixedLengthArray // ✅ OK
var [ a, b, c ] = myFixedLengthArray // ✅ OK
var [ a, b, c, d ] = myFixedLengthArray // ✅ INVALID INDEX ERROR
(*) 这个解决方案需要启用 noImplicitAny TypeScript 配置指令才能工作(通常推荐的做法)
类似数组的方法:
此解决方案表现为对 Array 类型的扩展,接受一个额外的第二个参数(数组长度)。它不像 基于元组的解决方案 那样严格和安全。
语法示例:
let foo: FixedLengthArray<string, 3>
请记住,这种方法不会阻止你访问声明边界之外的索引并对其设置值。
实施:
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift'
type FixedLengthArray<T, L extends number, TObj = [T, ...Array<T>]> =
Pick<TObj, Exclude<keyof TObj, ArrayLengthMutationKeys>>
& {
readonly length: L
[ I : number ] : T
[Symbol.iterator]: () => IterableIterator<T>
}
测试:
var myFixedLengthArray: FixedLengthArray<string,3>
// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ] // ✅ OK
myFixedLengthArray = [ 'a', 'b', 123 ] // ✅ TYPE ERROR
myFixedLengthArray = [ 'a' ] // ✅ LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ] // ✅ LENGTH ERROR
// Index assignment tests
myFixedLengthArray[1] = 'foo' // ✅ OK
myFixedLengthArray[1000] = 'foo' // ❌ SHOULD FAIL
// Methods that mutate array length
myFixedLengthArray.push('foo') // ✅ MISSING METHOD ERROR
myFixedLengthArray.pop() // ✅ MISSING METHOD ERROR
// Direct length manipulation
myFixedLengthArray.length = 123 // ✅ READ-ONLY ERROR
// Destructuring
var [ a ] = myFixedLengthArray // ✅ OK
var [ a, b ] = myFixedLengthArray // ✅ OK
var [ a, b, c ] = myFixedLengthArray // ✅ OK
var [ a, b, c, d ] = myFixedLengthArray // ❌ SHOULD FAIL
答案#3
最初的答案是在一段时间前用 TypeScript 3.x 版本编写的。从那时起,TypeScript 版本已经更新到 4.94,一些 TypeScript 的限制已经被解除。此外,由于评论中指出的一些问题,答案也做了修改。
原始答案
实际上,你可以用当前的 TypeScript 实现这一点:
type Grow<T, A extends Array<T>> =
((x: T, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never;
type GrowToSize<T, A extends Array<T>, N extends number> =
{ 0: A, 1: GrowToSize<T, Grow<T, A>, N> }[A['length'] extends N ? 0 : 1];
export type FixedArray<T, N extends number> = GrowToSize<T, [], N>;
示例:
// OK
const fixedArr3: FixedArray<string, 3> = ['a', 'b', 'c'];
// Error:
// Type '[string, string, string]' is not assignable to type '[string, string]'.
// Types of property 'length' are incompatible.
// Type '3' is not assignable to type '2'.ts(2322)
const fixedArr2: FixedArray<string, 2> = ['a', 'b', 'c'];
// Error:
// Property '3' is missing in type '[string, string, string]' but required in type
// '[string, string, string, string]'.ts(2741)
const fixedArr4: FixedArray<string, 4> = ['a', 'b', 'c'];
当时(TypeScript 3.x),采用这种方法可以构建相对较小的元组,大小最多为20个元素。对于更大的大小,会出现“类型实例化过于深且可能无限”的错误。这个问题是在下面的评论中由 @Micha Schwab 提出的。这让我考虑到更高效的增长数组的方法,从而产生了编辑1。
编辑 1:更大的尺寸(或“指数增长”)
这应该能够处理更大的尺寸(基本上它会指数级地增长数组,直到我们达到最接近的二的幂):
type Shift<A extends Array<any>> =
((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never;
type GrowExpRev<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {
0: GrowExpRev<[...A, ...P[0]], N, P>,
1: GrowExpRev<A, N, Shift<P>>
}[[...A, ...P[0]][N] extends undefined ? 0 : 1];
type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {
0: GrowExp<[...A, ...A], N, [A, ...P]>,
1: GrowExpRev<A, N, P>
}[[...A, ...A][N] extends undefined ? 0 : 1];
export type FixedSizeArray<T, N extends number> = N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T, T], N, [[T]]>;
这种方法可以处理更大的元组大小(最多可达 2^15),尽管对于超过 2^13 的数字,它明显变慢。
这种方法在处理 any、never 和 undefined 的元组时也存在问题。这些类型满足 extends undefined ? 条件(该条件用于测试索引是否超出生成的数组),因此会导致递归无限进行。这个问题由 @Victor Zhou 在他的评论中报告。
编辑 2:never、any 或 undefined 的元组
“指数数组增长”方法无法处理 any、never 和 undefined 的元组。这可以通过先准备一个“无争议类型”的元组,然后将元组重写为所需大小的所需项类型来解决。
type MapItemType<T, I> = { [K in keyof T]: I };
export type FixedSizeArray<T, N extends number> =
N extends 0 ? [] : MapItemType<GrowExp<[0], N, []>, T>;
示例:
var tupleOfAny: FixedSizeArray<any, 3>; // [any, any, any]
var tupleOfNever: FixedSizeArray<never, 3>; // [never, never, never]
var tupleOfUndef: FixedSizeArray<undefined, 2>; // [undefined, undefined]
与此同时,当前的 TypeScript 版本已经变为 4.94。是时候总结并清理代码了。
编辑 3:TypeScript 4.94
原始的 FixedArray 类型现在可以简单地写成如下方式:
type GrowToSize<T, N extends number, A extends T[]> =
A['length'] extends N ? A : GrowToSize<T, N, [...A, T]>;
export type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
现在这可以处理最大到999的尺寸。
let tuple999: FixedArray<boolean, 999>;
// let tuple999: [boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, ... 980 more ..., boolean]
let tuple1000: FixedArray<boolean, 1000>;
// let tuple1000: any
// Error:
// Type instantiation is excessively deep and possibly infinite. ts(2589)
所以我们可以添加安全措施,如果元组大小超过999,则返回T数组。
type GrowToSize<T, N extends number, A extends T[], L extends number = A['length']> =
L extends N ? A : L extends 999 ? T[] : GrowToSize<T, N, [...A, T]>;
export type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
let tuple3: FixedArray<boolean, 3>; // [boolean, boolean, boolean]
let tuple1000: FixedArray<boolean, 1000>; // boolean[]
“指数数组增长”方法现在可以处理最多 8192 (2^13) 元组大小。
超过该大小,它会引发“Type produces a tuple type that is too large to represent. ts(2799)”错误。
我们可以将其写入,包括大小为8192的保护,如下所示:
type Shift<A extends Array<any>> =
((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never;
type GrowExpRev<A extends any[], N extends number, P extends any[][]> =
A['length'] extends N ? A : [...A, ...P[0]][N] extends undefined ? GrowExpRev<[...A, ...P[0]], N, P> : GrowExpRev<A, N, Shift<P>>;
type GrowExp<A extends any[], N extends number, P extends any[][], L extends number = A['length']> =
L extends N ? A : L extends 8192 ? any[] : [...A, ...A][N] extends undefined ? GrowExp<[...A, ...A], N, [A, ...P]> : GrowExpRev<A, N, P>;
type MapItemType<T, I> = { [K in keyof T]: I };
export type FixedSizeArray<T, N extends number> =
N extends 0 ? [] : MapItemType<GrowExp<[0], N, []>, T>;
let tuple8192: FixedSizeArray<boolean, 8192>;
// let tuple8192: [boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, ... 8173 more ..., boolean]
let tuple8193: FixedSizeArray<boolean, 8193>;
// let tuple8193: boolean[]
答案#4
使用 TypeScript v4.6,这里是基于 Tomasz Gawel 的回答的一个超短版本
type Tuple<
T,
N extends number,
R extends readonly T[] = [],
> = R['length'] extends N ? R : Tuple<T, N, readonly [T, ...R]>;
// usage
const x: Tuple<number,3> = [1,2,3];
x; // resolves as [number, number, number]
x[0]; // resolves as number
还有其他方法可以强制设置 length 属性的值,但这并不是很优雅
// TLDR, don't do this
type Tuple<T, N> = { length: N } & readonly T[];
const x : Tuple<number,3> = [1,2,3]
x; // resolves as { length: 3 } | number[], which is kinda messy
x[0]; // resolves as number | undefined, which is incorrect
答案#5
虽然有点晚了,但如果你使用只读数组([] as const),这里有一种方法 -
interface FixedLengthArray<L extends number, T> extends ArrayLike<T> {
length: L
}
export const a: FixedLengthArray<2, string> = ['we', '432'] as const
在 const a 值中添加或删除字符串会导致此错误 -
Type 'readonly ["we", "432", "fd"]' is not assignable to type 'FixedLengthArray<2, string>'.
Types of property 'length' are incompatible.
Type '3' is not assignable to type '2'.ts(2322)
或
Type 'readonly ["we"]' is not assignable to type 'FixedLengthArray<2, string>'.
Types of property 'length' are incompatible.
Type '1' is not assignable to type '2'.ts(2322)
分别。
编辑(2022/05/13):相关的未来 TypeScript 功能 - 在此定义的 satisfies
