← Курс/Массивы и кортежи#142 из 257+25 XP

Массивы и кортежи в TypeScript

Типизированные массивы

В TypeScript массивы имеют тип элементов. Два эквивалентных синтаксиса:

// Синтаксис 1: тип[]
const names: string[] = ['Алексей', 'Мария', 'Иван']
const scores: number[] = [95, 87, 100]

// Синтаксис 2: Array<тип> (дженерик)
const names: Array<string> = ['Алексей', 'Мария', 'Иван']
const scores: Array<number> = [95, 87, 100]

TypeScript не позволит добавить элемент не того типа:

const nums: number[] = [1, 2, 3]
nums.push(4)        // OK
nums.push('пять')   // Ошибка: Argument of type 'string' is not assignable to parameter of type 'number'

Readonly массивы

readonly string[] или ReadonlyArray<string> — массив, который нельзя изменить:

const COLORS: readonly string[] = ['red', 'green', 'blue']

COLORS.push('yellow')  // Ошибка: Property 'push' does not exist on type 'readonly string[]'
COLORS[0] = 'black'    // Ошибка: Index signature in type 'readonly string[]' only permits reading

// Но читать можно:
console.log(COLORS[0])  // 'red'
console.log(COLORS.length)  // 3
console.log(COLORS.map(c => c.toUpperCase()))  // ['RED', 'GREEN', 'BLUE'] — не мутирует

Кортежи (Tuples)

Кортеж — массив **фиксированной длины** с **определёнными типами** на каждой позиции:

// Обычный массив — все элементы одного типа
const arr: number[] = [1, 2, 3, 4, 5]  // любая длина

// Кортеж — фиксированная структура
type Point = [number, number]
const p: Point = [10, 20]

type Entry = [string, number, boolean]
const user: Entry = ['Алексей', 30, true]

// Деструктуризация кортежа
const [name, age, isActive] = user
console.log(name)      // 'Алексей'
console.log(age)       // 30
console.log(isActive)  // true

Именованные кортежи

TypeScript 4.0+ поддерживает именованные элементы кортежа — улучшает читаемость:

type Point3D = [x: number, y: number, z: number]
type UserRecord = [name: string, age: number, email: string]

const point: Point3D = [1, 2, 3]
const record: UserRecord = ['Алексей', 30, 'alex@mail.ru']

Rest в кортежах

Кортежи могут содержать rest-элементы — полезно для описания функций с переменным числом аргументов:

type StringsAndNumber = [...string[], number]
const example: StringsAndNumber = ['a', 'b', 'c', 42]  // последний всегда number

type AtLeastTwo = [string, string, ...string[]]
const tags: AtLeastTwo = ['js', 'ts', 'react', 'vue']  // минимум 2 строки

Практическое применение кортежей

Кортежи часто используются для возвращения нескольких значений из функции (как в React useState):

function useState<T>(initial: T): [T, (value: T) => void] {
  let state = initial
  const setState = (value: T) => { state = value }
  return [state, setState]
}

const [count, setCount] = useState(0)
// count имеет тип number
// setCount имеет тип (value: number) => void

Примеры

Runtime валидация кортежей — создание, проверка структуры и операции с типизированными парами координат

// TypeScript проверяет кортежи при компиляции.
// Реализуем runtime-валидацию для демонстрации концепции.

// Создаёт и валидирует кортеж [x, y] — оба должны быть числами
function createPoint(x, y) {
  if (typeof x !== 'number' || typeof y !== 'number') {
    throw new TypeError(`createPoint ожидает два числа, получено [${typeof x}, ${typeof y}]`)
  }
  return [x, y]
}

// Валидирует что значение является кортежем [number, number]
function isPoint(value) {
  return (
    Array.isArray(value) &&
    value.length === 2 &&
    typeof value[0] === 'number' &&
    typeof value[1] === 'number'
  )
}

// Складывает два кортежа-точки
function addPoints(p1, p2) {
  if (!isPoint(p1) || !isPoint(p2)) {
    throw new TypeError('Оба аргумента должны быть кортежами [number, number]')
  }
  const [x1, y1] = p1
  const [x2, y2] = p2
  return createPoint(x1 + x2, y1 + y2)
}

// Вычисляет расстояние между двумя точками
function distance(p1, p2) {
  if (!isPoint(p1) || !isPoint(p2)) {
    throw new TypeError('Оба аргумента должны быть кортежами [number, number]')
  }
  const [x1, y1] = p1
  const [x2, y2] = p2
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
}

// Масштабирует точку
function scalePoint(point, factor) {
  if (!isPoint(point) || typeof factor !== 'number') {
    throw new TypeError('scalePoint: нужна точка и числовой коэффициент')
  }
  const [x, y] = point
  return createPoint(x * factor, y * factor)
}

// Симуляция readonly — возвращает замороженный кортеж
function createReadonlyPoint(x, y) {
  return Object.freeze(createPoint(x, y))
}

// --- Демонстрация ---
console.log('=== Создание точек ===')
const p1 = createPoint(3, 4)
const p2 = createPoint(6, 8)
console.log('p1:', p1)   // [3, 4]
console.log('p2:', p2)   // [6, 8]

console.log('\n=== Операции с точками ===')
console.log('p1 + p2:', addPoints(p1, p2))    // [9, 12]
console.log('distance:', distance(p1, p2))    // 5
console.log('p1 * 2:', scalePoint(p1, 2))     // [6, 8]

console.log('\n=== Деструктуризация кортежей ===')
const [x, y] = p1
console.log(`x=${x}, y=${y}`)  // x=3, y=4

console.log('\n=== Readonly кортеж ===')
const frozen = createReadonlyPoint(1, 2)
console.log('frozen:', frozen)
try {
  frozen[0] = 99  // В strict mode: TypeError
  console.log('frozen после изменения:', frozen)  // [1, 2] — не изменился
} catch (e) {
  console.log('Ошибка изменения readonly:', e.message)
}

console.log('\n=== Ошибки типов ===')
try {
  createPoint('10', 20)  // TypeError
} catch (e) {
  console.log(e.message)
}

try {
  addPoints([1, 2], [3, 4, 5])  // TypeError — не кортеж
} catch (e) {
  console.log(e.message)
}