Jak zadeklarować tablicę o stałej długości w języku TypeScript


110

Ryzykując wykazaniem braku wiedzy na temat typów TypeScript - mam następujące pytanie.

Kiedy tworzysz deklarację typu dla tablicy takiej jak ta ...

position: Array<number>;

... pozwoli ci stworzyć tablicę o dowolnej długości. Jeśli jednak chcesz mieć tablicę zawierającą liczby o określonej długości, tj. 3 dla komponentów x, y, z, czy możesz utworzyć typ z tablicą o stałej długości, coś takiego?

position: Array<3>

Każda pomoc lub wyjaśnienie mile widziane!

Odpowiedzi:


178

Tablica javascript posiada konstruktor akceptujący długość tablicy:

let arr = new Array<number>(3);
console.log(arr); // [undefined × 3]

Jest to jednak tylko rozmiar początkowy, nie ma ograniczeń co do zmiany tego:

arr.push(5);
console.log(arr); // [undefined × 3, 5]

Typescript ma typy krotek, które pozwalają zdefiniować tablicę o określonej długości i typach:

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

19
Typy krotek sprawdzają tylko rozmiar początkowy, więc arrpo zainicjowaniu nadal możesz przekazać nieograniczoną ilość „liczby” do swojego .
benjaminz

4
To prawda, że ​​w czasie wykonywania nadal działa javascript, aby „wszystko było możliwe”. Przynajmniej transpiler maszynopisu wymusi to przynajmniej w kodzie źródłowym
henryJack

7
W przypadku, gdy chcę mieć duże rozmiary tablic, takie jak, powiedzmy, 50, czy istnieje sposób na określenie rozmiaru tablicy za pomocą powtarzanego typu, na przykład [number[50]], aby nie trzeba było pisać [number, number, ... ]50 razy?
Victor Zamanian

2
Nieważne, znalazłem pytanie dotyczące tego. stackoverflow.com/questions/52489261/…
Victor Zamanian

1
@VictorZamanian Pamiętaj, że pomysł przecięcia {length: TLength}nie powoduje żadnego błędu maszynopisu, gdybyś przekroczył wpisany tekst TLength. Nie znalazłem jeszcze składni typu n długości narzucanej przez rozmiar.
Lucas Morgan

25

Podejście Tuple:

To rozwiązanie zapewnia ścisłą sygnaturę typu FixedLengthArray (aka. SealedArray) opartą na krotkach.

Przykład składni:

// Array containing 3 strings
let foo : FixedLengthArray<[string, string, string]> 

Jest to najbezpieczniejsze podejście, biorąc pod uwagę, że uniemożliwia dostęp do indeksów poza granicami .

Realizacja :

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> > }

Testy:

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

(*) To rozwiązanie wymaga włączenia dyrektywy konfiguracyjnej dlanoImplicitAny maszynopisu , aby działało (powszechnie zalecana praktyka)


Podejście Array (ish):

To rozwiązanie zachowuje się jak rozszerzenie Arraytypu, akceptujące dodatkowy drugi parametr (długość tablicy). Nie jest tak rygorystyczna i bezpieczna jak rozwiązanie oparte na Tuple .

Przykład składni:

let foo: FixedLengthArray<string, 3> 

Należy pamiętać, że takie podejście nie uniemożliwi dostępu do indeksu poza zadeklarowanymi granicami i nie ustawi dla niego wartości.

Realizacja :

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>   
  }

Testy:

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
Dzięki! Jednak nadal można zmienić rozmiar tablicy bez wyświetlania błędu.
Eduard

1
var myStringsArray: FixedLengthArray<string, 2> = [ "a", "b" ] // LENGTH ERRORwygląda na to, że 2 powinno być tutaj 3?
Qwertiy

Zaktualizowałem implementację za pomocą bardziej rygorystycznego rozwiązania, które zapobiega zmianom długości tablicy
colxi

@colxi Czy jest możliwa implementacja, która umożliwia mapowanie z FixedLengthArray do innych FixedLengthArray? Przykład tego, co mam na myśli:const threeNumbers: FixedLengthArray<[number, number, number]> = [1, 2, 3]; const doubledThreeNumbers: FixedLengthArray<[number, number, number]> = threeNumbers.map((a: number): number => a * 2);
Alex Malcolm

@AlexMalcolm Obawiam się, że mapdostarcza generyczny podpis tablicy na swoim wyjściu. W twoim przypadku najprawdopodobniej number[]typ
colxi

7

Właściwie możesz to osiągnąć za pomocą aktualnego maszynopisu:

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

Przykłady:

// 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
Jak tego użyć, gdy liczba elementów jest zmienną? Jeśli mam N jako typ liczby i „num” jako liczbę, to const arr: FixedArray <liczba, N> = Array.from (new Array (num), (x, i) => i); daje mi informację "Instancja typu jest zbyt głęboka i prawdopodobnie nieskończona".
Micha Schwab

2
@MichaSchwab Niestety wydaje się, że działa tylko ze stosunkowo małymi liczbami. W przeciwnym razie mówi „za dużo rekursji”. To samo dotyczy twojego przypadku. Nie przetestowałem tego dokładnie :(.
Tomasz Gawel

Dziękuję, że do mnie wróciłeś! Jeśli znajdziesz rozwiązanie dla zmiennej długości, daj mi znać.
Micha Schwab
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.