冒着显示我对TypeScript类型缺乏了解的风险,我有以下问题。

当你为这样的数组做类型声明时…

position: Array<number>;

...它可以让你创建任意长度的数组。然而,如果你想要一个包含特定长度的数字的数组,例如x y z组件的3,你可以为一个固定长度的数组创建一个类型,像这样吗?

position: Array<3>

任何帮助或澄清感谢!


当前回答

这有点晚了,但如果您使用只读数组([]作为const),这里有一种方法

interface FixedLengthArray<L extends number, T> extends ArrayLike<T> {
  length: L
}

export const a: FixedLengthArray<2, string> = ['we', '432'] as const

在const值中添加或删除字符串将导致此错误-

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)

OR

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)

分别。

编辑(05/13/2022):相关的未来TS功能-满足这里的定义

其他回答

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]'

元组方法:

该解决方案提供了一个严格的FixedLengthArray (aka .a。基于元组的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配置指令才能工作(通常推荐的做法)


数组(ish)方法:

此解决方案充当Array类型的扩充,接受额外的第二个参数(Array length)。不像基于元组的解决方案那样严格和安全。

语法示例:

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

最初的答案是在一段时间前写的,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在下面的评论中提出的。这使得我们考虑更有效的方法来增长数组,从而产生了Edit 1。

编辑1:更大的规模(或“指数增长”)

这应该可以处理更大的大小(基本上它会以指数方式增长数组,直到我们得到最接近2的幂):

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的元组时也有问题。这些类型满足扩展未定义?条件(用于测试索引是否超出生成的数组的条件),因此将使递归无限地进行。这个问题是由@Victor Zhou在评论中报告的。

编辑2:never、any或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生成的元组类型太大而无法表示。ts(2799)”。

我们可以这样写,包括8192尺寸的safe guard,如下:

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[]

这有点晚了,但如果您使用只读数组([]作为const),这里有一种方法

interface FixedLengthArray<L extends number, T> extends ArrayLike<T> {
  length: L
}

export const a: FixedLengthArray<2, string> = ['we', '432'] as const

在const值中添加或删除字符串将导致此错误-

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)

OR

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)

分别。

编辑(05/13/2022):相关的未来TS功能-满足这里的定义

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