Як оголосити масив фіксованої довжини в TypeScript


104

Ризикуючи продемонструвати мою невідомість щодо типів TypeScript - у мене таке питання.

Коли ви робите оголошення типу для такого масиву ...

position: Array<number>;

... це дозволить зробити масив з довільною довжиною. Однак якщо ви хочете, щоб масив, що містить числа з певною довжиною, тобто 3 для x, y, z компонентів, чи можете ви створити тип з масивом з фіксованою довжиною, щось подібне?

position: Array<3>

Будь-яка допомога чи роз’яснення вдячні!

Відповіді:


164

У масиві 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]'

18
Типи кортежів перевіряють лише початковий розмір, тому ви все ще можете надсилати необмежену кількість "числа" до свого arrпісля його ініціалізації.
Бенджамінц

4
Правда, це все ще JavaScript під час виконання, щоб "все проходило" в цей момент. Принаймні, транспілятор машинопису примусить це застосувати у вихідному коді принаймні
henryJack

6
У випадку, якщо я хочу великих розмірів масиву, наприклад, 50, чи є спосіб вказати розмір масиву з повторним типом, наприклад [number[50]], так що не потрібно було б писати [number, number, ... ]50 разів?
Віктор Заманян

2
Незважаючи на це, знайшли питання щодо цього. stackoverflow.com/questions/52489261 / ...
Victor Zamanian

1
@VictorZamanian Тільки для того, щоб ви знали, ідея перетину {length: TLength}не дає жодної помилки машинопису, якщо ви перевищуєте введенеTLength . Я ще не знайшов синтаксису типу n-length із розміром.
Лукас Морган

22

Підхід Tuple:

Це рішення забезпечує суворий підпис типу FixedLengthArray (ak.a. SealedArray), що базується в Tuples.

Приклад синтаксису:

// 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машинопис конфігурації директива повинна бути включена для того , щоб працювати (зазвичай рекомендується практика)


Підхід масиву (ish):

Це рішення поводиться як посилення Array типу, приймаючи додатковий другий параметр (довжина масиву). Це не так суворо і безпечно, як рішення на основі Tuple .

Приклад синтаксису:

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

1
Дякую! Однак, як і раніше можна змінити розмір масиву, не отримуючи помилки.
Едуард

1
var myStringsArray: FixedLengthArray<string, 2> = [ "a", "b" ] // LENGTH ERRORздається, 2 тут має бути 3?
Квертій,

Я оновив реалізацію більш суворим рішенням, яке запобігає зміні довжини масиву
colxi

@colxi Чи можна мати реалізацію, яка дозволяє зіставляти з FixedLengthArray в інші FixedLengthArray? Приклад того, що я маю на увазі:const threeNumbers: FixedLengthArray<[number, number, number]> = [1, 2, 3]; const doubledThreeNumbers: FixedLengthArray<[number, number, number]> = threeNumbers.map((a: number): number => a * 2);
Алекс Малкольм

@AlexMalcolm Я боюся, що mapна його вихід виводиться загальний підпис масиву. У вашому випадку, швидше за все, number[]тип
колксі

5

Насправді, ви можете досягти цього за допомогою поточного шрифту:

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

1
Як це використовувати, коли кількість елементів є змінною? Якщо у мене число N як тип числа, а "число" як число, то const arr: FixedArray <число, N> = Array.from (новий масив (num), (x, i) => i); дає мені "Тип екземпляра надмірно глибокий і, можливо, нескінченний".
Micha Schwab

2
@MichaSchwab На жаль, схоже, це працює лише з відносно невеликими цифрами. В іншому випадку це говорить "занадто багато рекурсії". Те саме стосується і вашої справи. Я не перевіряв це ретельно :(.
Томаш Гавель

Дякую, що повернулися до мене! Якщо ви все-таки знайдете рішення різної довжини, будь ласка, повідомте мене про це.
Micha Schwab
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.