Avatar ·

Как объявить массив фиксированной длины в TypeScript?

Привет! Я недавно начал изучать TypeScript и столкнулся с проблемой. Мне нужно объявить массив, который всегда будет содержать строго определённое количество элементов — например, ровно 3 числа. Я пробовал писать так: ```typescript let arr: number[3] = [1, 2, 3]; ``` Но TypeScript ругается: `'number[3]' refers to a value, but is being used as a type here`. Потом я попробовал: ```typescript let arr: [number, number, number] = [1, 2, 3]; ``` Это сработало! Но я хочу понять, есть ли более общий способ создать массив фиксированной длины, например, для 100 элементов? Вручную писать `[number, number, number, ... 100 раз]` — это же ужасно. Ещё я заметил, что если я напишу: ```typescript let arr: [number, number, number] = [1, 2, 3]; arr.push(4); // не ругается ``` TypeScript не выдаёт ошибку при добавлении нового элемента! Разве это не нарушает идею фиксированной длины? Как сделать так, чтобы массив действительно был строго фиксированной длины и нельзя было добавить или удалить элементы? Буду благодарен за подробное объяснение с примерами. Спасибо! <<>>

Корректное объявление массивов фиксированной длины в TypeScript

Вы на правильном пути — кортежи (tuple) это именно то, что нужно. Однако есть нюансы, которые важно понимать. Давайте разберём всё по порядку.

1. Базовый синтаксис: кортежи

Кортеж — это массив с фиксированным количеством элементов, где каждый элемент может иметь свой тип. Объявляется он так:

let point: [number, number] = [10, 20]; // ровно 2 числа
let rgb: [number, number, number] = [255, 0, 0]; // ровно 3 числа
let person: [string, number] = ["Alice", 30]; // строка и число

Важно: кортежи не защищают от методов push, pop, splice и т.д. Это известное ограничение TypeScript. Компилятор не проверяет длину массива во время выполнения — только типы элементов.

2. Как запретить изменение длины кортежа

Используйте модификатор readonly:

let point: readonly [number, number] = [10, 20];
point.push(30); // Ошибка: Свойство 'push' не существует в типе 'readonly [number, number]'
point[0] = 5;   // Ошибка: Индексная сигнатура в типе 'readonly [number, number]' допускает только чтение

Теперь массив полностью защищён от изменений — ни длину, ни значения элементов изменить нельзя. Это идеальный вариант для фиксированной длины.

3. Как создать кортеж на N элементов (без ручного перечисления)

Если нужно 100 элементов одного типа, писать 100 раз number неудобно. Используйте вспомогательный тип:

// Создаём тип-кортеж заданной длины
type Tuple<T, N extends number> = N extends N 
  ? number extends N ? T[] : _TupleOf<T, N, []> 
  : never;
type _TupleOf<T, N extends number, R extends unknown[]> = 
  R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;

// Пример использования
type FixedArray100 = Tuple<number, 100>;
let arr: FixedArray100 = new Array(100).fill(0); // ок
let arr2: FixedArray100 = [1, 2]; // Ошибка: Source has 2 element(s) but target requires 100

Это рекурсивный тип, который генерирует кортеж нужной длины. Работает для небольших N (до ~50-100). Для больших значений лучше использовать обычные массивы с проверкой во время выполнения.

4. Альтернатива: проверка длины в рантайме

Если вам критично контролировать длину именно во время выполнения (например, для 1000+ элементов), используйте класс-обёртку:

class FixedArray<T> {
  private data: T[];
  constructor(private length: number, ...items: T[]) {
    if (items.length !== length) {
      throw new Error(`Expected ${length} elements, got ${items.length}`);
    }
    this.data = items;
  }
  get(index: number): T {
    if (index < 0 || index >= this.length) throw new Error("Index out of bounds");
    return this.data[index];
  }
  // ... другие методы
}

const arr = new FixedArray(3, 1, 2, 3); // ок
const bad = new FixedArray(3, 1, 2); // Ошибка в runtime

5. Современный подход: variadic tuple types (TypeScript 4.0+)

Начиная с TS 4.0 можно использовать spread в кортежах для динамического создания:

// Функция, принимающая ровно 3 числа
function setCoordinates(...args: [number, number, number]) {
  // args — это кортеж [number, number, number]
}
setCoordinates(1, 2, 3); // ок
setCoordinates(1, 2);    // Ошибка: Expected 3 arguments

// generic подход для произвольной длины
type ArrayOfLength<T, N extends number> = 
  N extends 0 ? [] :
  N extends 1 ? [T] :
  N extends 2 ? [T, T] :
  N extends 3 ? [T, T, T] :
  T[]; // fallback для больших N

Итоговые рекомендации

  • Для 2–5 элементов используйте обычный кортеж: [number, number]
  • Для защиты от изменений добавляйте readonly: readonly [number, number]
  • Для больших фиксированных массивов (до ~50) используйте рекурсивный тип Tuple
  • Для очень больших массивов — проверка длины в рантайме через класс или функцию
  • Помните: кортежи — это синтаксический сахар, они не дают гарантий на уровне выполнения, только на уровне типов

Надеюсь, теперь стало понятнее! Если остались вопросы — спрашивайте.

<<>>

Использование утилитарных типов из библиотек

Если вам не хочется писать рекурсивные типы вручную, можно воспользоваться готовыми решениями из популярных библиотек.

Вариант 1: TypeScript Utility Types (ts-essentials)

Установите пакет:

npm install ts-essentials

Используйте тип FixedLengthArray:

import { FixedLengthArray } from 'ts-essentials';

type Point = FixedLengthArray<number, 2>; // [number, number]
type RGB = FixedLengthArray<number, 3>;   // [number, number, number]

let arr: RGB = [255, 0, 0]; // ок
let bad: RGB = [255, 0];    // Ошибка: Type '[number, number]' is not assignable to type '[number, number, number]'

Вариант 2: Самодельный тип с помощью mapped types

Можно использовать более простой подход без рекурсии — через условные типы и mapped tuples (доступно с TS 4.1):

type FixedArray<T, N extends number> = 
  { [K in keyof TupleOf<T, N>]: T };

type TupleOf<T, N extends number, R extends any[] = []> = 
  R['length'] extends N ? R : TupleOf<T, N, [...R, T]>;

// Тест
type Test = FixedArray<number, 5>; // [number, number, number, number, number]

Вариант 3: Простое решение для небольших длин

Если длина не превышает 10–15, можно просто перечислить все варианты в union:

type ArrayOf3 = [any, any, any];
type ArrayOf4 = [any, any, any, any];
// ... и так далее

Это неэлегантно, но работает и легко читается.

Выбирайте подход, который лучше подходит под вашу задачу. Для большинства случаев readonly кортеж — оптимальное решение.