Cómo declarar una matriz de longitud fija en TypeScript


104

A riesgo de demostrar mi falta de conocimiento sobre los tipos de TypeScript, tengo la siguiente pregunta.

Cuando haces una declaración de tipo para una matriz como esta ...

position: Array<number>;

... te permitirá hacer una matriz con una longitud arbitraria. Sin embargo, si desea una matriz que contenga números con una longitud específica, es decir, 3 para los componentes x, y, z, ¿puede hacer un tipo con una matriz de longitud fija, algo como esto?

position: Array<3>

Se agradece cualquier ayuda o aclaración!

Respuestas:


164

La matriz javascript tiene un constructor que acepta la longitud de la matriz:

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

Sin embargo, este es solo el tamaño inicial, no hay restricciones para cambiar eso:

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

TypeScript tiene tipos de tuplas que le permiten definir una matriz con una longitud y 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]'

18
Los tipos de tupla verifican solo el tamaño inicial, por lo que aún puede enviar una cantidad ilimitada de "número" a su arrdespués de que se inicialice.
benjaminz

4
Es cierto que todavía es javascript en tiempo de ejecución para "todo vale" en ese momento. Al menos el transpilador mecanografiado hará cumplir esto en el código fuente al menos
henryJack

6
En caso de que quiera tamaños de matriz grandes como, digamos, 50, ¿hay alguna manera de especificar el tamaño de la matriz con un tipo repetido, como [number[50]], para que no sea necesario escribir [number, number, ... ]50 veces?
Victor Zamanian

2
No importa, encontré una pregunta al respecto. stackoverflow.com/questions/52489261/…
Victor Zamanian

1
@VictorZamanian Solo para que lo sepas, la idea de intersecar {length: TLength}no proporciona ningún error de mecanografía en caso de que superes el escrito TLength. Todavía no he encontrado una sintaxis de tipo de longitud n reforzada por tamaño.
Lucas Morgan

22

El enfoque Tuple:

Esta solución proporciona una firma de tipo FixedLengthArray estricta (también conocida como SealedArray) basada en Tuples.

Ejemplo de sintaxis:

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

Este es el enfoque más seguro, considerando que evita acceder a índices fuera de los límites .

Implementación:

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

Pruebas:

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 solución requiere que la directiva de configuración denoImplicitAny mecanografiado esté habilitada para funcionar (práctica comúnmente recomendada)


El enfoque Array (ish):

Esta solución se comporta como un aumento del Arraytipo, aceptando un segundo parámetro adicional (Array length). No es tan estricto y seguro como la solución basada en Tuple .

Ejemplo de sintaxis:

let foo: FixedLengthArray<string, 3> 

Tenga en cuenta que este enfoque no le impedirá acceder a un índice fuera de los límites declarados y establecer un valor en él.

Implementación:

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

Pruebas:

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
¡Gracias! Sin embargo, aún es posible cambiar el tamaño de la matriz sin obtener un error.
Eduard

1
var myStringsArray: FixedLengthArray<string, 2> = [ "a", "b" ] // LENGTH ERRORparece que 2 deberían ser 3 aquí?
Qwertiy


@colxi ¿Es posible tener una implementación que permita el mapeo de FixedLengthArray a otros FixedLengthArray? Un ejemplo de lo que quiero decir: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 me temo que mapproporciona una firma de matriz genérica a su salida. En su caso, lo más probable es que sea un number[]tipo
colxi

5

En realidad, puede lograr esto con el mecanografiado actual:

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

Ejemplos:

// 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
¿Cómo uso esto cuando el número de elementos es una variable? Si tengo N como tipo de número y "num" como número, entonces const arr: FixedArray <número, N> = Array.from (new Array (num), (x, i) => i); me da "La instanciación de tipos es excesivamente profunda y posiblemente infinita".
Micha Schwab

2
@MichaSchwab Desafortunadamente, parece funcionar solo con números relativamente pequeños. De lo contrario, dice "demasiada recursividad". Lo mismo se aplica a su caso. No lo
probé a

¡Gracias por volver a mí! Si encuentra una solución para longitud variable, hágamelo saber.
Micha Schwab
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.