Como declarar uma matriz de comprimento fixo no TypeScript

104

Correndo o risco de demonstrar minha falta de conhecimento sobre os tipos do TypeScript - tenho a seguinte pergunta.

Quando você faz uma declaração de tipo para uma matriz como esta ...

position: Array<number>;

... permitirá que você faça uma matriz com comprimento arbitrário. No entanto, se você quiser uma matriz contendo números com um comprimento específico, ou seja, 3 para componentes x, y, z, você pode fazer um tipo com uma matriz de comprimento fixo, algo assim?

position: Array<3>

Qualquer ajuda ou esclarecimento apreciada!

HenryJack
fonte

Respostas:

164

A matriz javascript tem um construtor que aceita o comprimento da matriz:

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

No entanto, este é apenas o tamanho inicial, não há restrição para alterá-lo:

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

Typescript tem tipos de tupla que permitem definir uma matriz com um comprimento e tipos específicos:

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]'
Nitzan Tomer
fonte
18
Os tipos de tupla verificam apenas o tamanho inicial, então você ainda pode enviar uma quantidade ilimitada de "número" para seu arrdepois de inicializado.
benjaminz
4
É verdade que ainda é javascript em tempo de execução para "vale tudo" nesse ponto. Pelo menos o transpiler de texto digitado aplicará isso no código-fonte, pelo menos
henryJack
6
No caso de eu querer tamanhos de array grandes como, digamos, 50, há uma maneira de especificar o tamanho do array com um tipo repetido, como [number[50]], de forma que não seja necessário escrever [number, number, ... ]50 vezes?
Victor Zamanian
2
Não importa, encontrei uma pergunta a respeito disso. stackoverflow.com/questions/52489261/…
Victor Zamanian
1
@VictorZamanian Só para você saber, a ideia de intersecção {length: TLength}não fornece nenhum erro de digitação caso você exceda o digitado TLength. Eu ainda não encontrei uma sintaxe de tipo de comprimento n imposta por tamanho.
Lucas Morgan,
22

A abordagem Tupla:

Esta solução fornece uma assinatura do tipo FixedLengthArray (também conhecido como SealedArray) baseada em Tuplas.

Exemplo de sintaxe:

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

Essa é a abordagem mais segura, considerando que evita o acesso a índices fora dos limites .

Implementação:

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

Testes:

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

(*) Esta solução requer que a diretiva de configuração denoImplicitAny typescript esteja habilitada para funcionar (prática comumente recomendada)


A abordagem Array (ish):

Esta solução se comporta como um aumento do Arraytipo, aceitando um segundo parâmetro adicional (comprimento do array). Não é tão estrito e seguro quanto a solução baseada em Tupla .

Exemplo de sintaxe:

let foo: FixedLengthArray<string, 3> 

Lembre-se de que essa abordagem não impedirá que você acesse um índice fora dos limites declarados e defina um valor para ele.

Implementação:

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

Testes:

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
Colxi
fonte
1
Obrigado! No entanto, ainda é possível alterar o tamanho da matriz sem obter um erro.
Eduard
1
var myStringsArray: FixedLengthArray<string, 2> = [ "a", "b" ] // LENGTH ERRORparece que 2 deveria ser 3 aqui?
Qwertiy
Atualizei a implementação com uma solução mais rigorosa que evita alterações no comprimento do array
colxi
@colxi É possível ter uma implementação que permite o mapeamento de FixedLengthArray para outros FixedLengthArray? Um exemplo do que quero dizer: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, receio, mapfornece uma assinatura de array genérica para sua saída. No seu caso, provavelmente um number[]tipo
colxi
5

Na verdade, você pode conseguir isso com o texto datilografado atual:

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

Exemplos:

// 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'];
Tomasz Gawel
fonte
1
Como faço para usar isso quando o número de elementos é uma variável? Se eu tiver N como o tipo de número e "num" como o número, então const arr: FixedArray <number, N> = Array.from (new Array (num), (x, i) => i); me dá "A instanciação do tipo é excessivamente profunda e possivelmente infinita".
Micha Schwab
2
@MichaSchwab Infelizmente, parece funcionar apenas com números relativamente pequenos. Caso contrário, informa "demasiada recursão". O mesmo se aplica ao seu caso. Eu não testei completamente :(.
Tomasz Gawel
Obrigado por entrar em contato comigo! Se você encontrar uma solução para comprimento variável, por favor me avise.
Micha Schwab