REST в JavaScript и TypeScript

REST-оператор (...) работает по-разному в зависимости от контекста использования. Существует три основных контекста с разными правилами расположения.

В параметрах функций (JavaScript runtime): REST может быть только последним параметром. Это жёсткое ограничение движка JavaScript, который обрабатывает аргументы последовательно слева направо.

При деструктуризации массивов (JavaScript runtime): REST также должен быть последним элементом в паттерне деструктуризации. Работа идёт с существующим массивом, но синтаксические правила аналогичны параметрам функций.

В условных типах TypeScript (compile-time): REST с infer может располагаться в любой позиции - начале, середине или конце. Система типов работает на этапе компиляции и имеет полную информацию о структуре типов, что снимает ограничения runtime.

// ❌ Параметры функций - REST только справа
function wrong(...rest, last) { }  // SyntaxError

// ❌ Деструктуризация - REST только справа  
const [first, ...middle, last] = arr  // SyntaxError

// ✅ TypeScript типы - REST в любой позиции
type Head<T> = T extends [...infer Rest, infer Last] ? Rest : never  // OK!
type Tail<T> = T extends [infer First, ...infer Rest] ? Rest : never  // OK!

JavaScript: Runtime-поведение

Параметры функций

REST-параметр собирает все "лишние" аргументы в массив. Он обязательно должен быть последним, потому что интерпретатор обрабатывает аргументы последовательно - обычные параметры получают значения по порядку, а REST забирает всё оставшееся.

// ✅ Правильное использование
function createUser(name, age, ...hobbies) {
  return {
    name,
    age,
    hobbies  // массив всех переданных хобби
  }
}

const user = createUser('Иван', 25, 'Футбол', 'Чтение', 'Код')
// { name: 'Иван', age: 25, hobbies: ['Футбол', 'Чтение', 'Код'] }

// ✅ REST может быть единственным параметром
function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0)
}

sum(1, 2, 3, 4, 5);  // 15

// ✅ Комбинация с обычными параметрами
function log(level, ...messages) {
  console.log(`[${level}]`, ...messages)
}

log('INFO', 'Пользователь', 'вошёл', 'в систему')
// [INFO] Пользователь вошёл в систему

Почему только в конце? Движок не может заранее знать, сколько аргументов будет передано. Если REST стоит не последним, возникает неоднозначность - невозможно определить, где заканчивается "остальное".

// ❌ Это физически невозможно реализовать
// function process(...middle, last) { }
// Сколько аргументов отдать в middle? Как определить last?

Деструктуризация массивов

При деструктуризации работа идёт с уже существующим массивом. REST собирает оставшиеся элементы массива в новый массив. Правило остаётся тем же - REST должен быть последним.

// ✅ Извлечение первого элемента и остальных
const tasks = ['Почта', 'Отчёт', 'Звонок', 'Встреча']
const [current, ...remaining] = tasks

console.log(current)     // 'Почта'
console.log(remaining)   // ['Отчёт', 'Звонок', 'Встреча']

// ✅ Извлечение нескольких первых элементов
const scores = [95, 87, 92, 78, 85]
const [gold, silver, bronze, ...others] = scores

console.log(gold, silver, bronze)  // 95 87 92
console.log(others)                // [78, 85]

// ✅ Пропуск элементов через пустые слоты
const data = ['skip', 'skip', 'need', 'also need', 'and this']
const [, , important, alsoImportant, ...rest] = data

console.log(important)       // 'need'
console.log(alsoImportant)  // 'also need'
console.log(rest)           // ['and this']

// ✅ Вложенная деструктуризация
const users = [
  { name: 'Алексей', skills: ['JS', 'React', 'Node'] },
  { name: 'Мария', skills: ['Python', 'Django'] }
]

const [{ name, skills: [main, ...other] }, ...otherUsers] = users
console.log(name)        // 'Алексей'
console.log(main)        // 'JS'
console.log(other)       // ['React', 'Node']
console.log(otherUsers)  // [{ name: 'Мария', ... }]

Как получить последний элемент? Поскольку REST может быть только в конце, нужно использовать обходные пути.

const numbers = [1, 2, 3, 4, 5]

// Вариант 1: через индекс
const last = numbers[numbers.length - 1]  // 5

// Вариант 2: через деструктуризацию с пропусками (если известна длина)
const [, , , , lastNum] = numbers  // 5

// Вариант 3: комбинация REST и индекса
const [...allButLast] = numbers
const lastElement = allButLast.pop()  // 5

Практические паттерны

// Паттерн 1: Разделение головы и хвоста списка
function processQueue(queue) {
  const [next, ...remaining] = queue
  if (!next) return

  handleTask(next)
  processQueue(remaining)
}

// Паттерн 2: Группировка аргументов
function addToCart(userId, ...items) {
  return {
    userId,
    items,
    total: items.reduce((sum, item) => sum + item.price, 0)
  }
}

const cart = addToCart('user123',
  { id: 1, price: 100 },
  { id: 2, price: 200 },
  { id: 3, price: 150 }
)

// Паттерн 3: Извлечение конфигурации
const config = ['production', 'localhost', 8080, 'debug', 'verbose']
const [env, host, port, ...flags] = config

console.log({ env, host, port, flags })
// { env: 'production', host: 'localhost', port: 8080, flags: ['debug', 'verbose'] }

// Паттерн 4: Работа с API-ответами
function handleApiResponse(response) {
  const [status, message, ...data] = response

  if (status !== 'success') {
    throw new Error(message)
  }

  return data  // возвращаем только полезные данные
}

TypeScript: Compile-time манипуляции

Условные типы с infer

В системе типов TypeScript REST с infer может располагаться в любой позиции. Это возможно потому, что проверка типов происходит до выполнения кода, когда известна полная структура типов.

// ✅ REST слева - извлечение последнего элемента
type Last<T extends any[]> = T extends [...infer Rest, infer L] ? L : never

type LastType = Last<[string, number, boolean]>  // boolean
type EmptyLast = Last<[]>  // never

// ✅ REST справа - извлечение первого элемента
type First<T extends any[]> = T extends [infer F, ...infer Rest] ? F : never

type FirstType = First<[string, number, boolean]>  // string
type EmptyFirst = First<[]>  // never

// ✅ REST в середине - извлечение крайних элементов
type Edges<T extends any[]> = T extends [infer F, ...any[], infer L]
  ? [F, L]
  : never

type EdgeTypes = Edges<[string, number, boolean, symbol]>  // [string, symbol]

Utility-типы для работы с кортежами

// Получение всех элементов кроме первого
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : []

type Example1 = Tail<[1, 2, 3, 4]>  // [2, 3, 4]
type Example2 = Tail<[1]>           // []
type Example3 = Tail<[]>            // []

// Получение всех элементов кроме последнего
type Init<T extends any[]> = T extends [...infer Rest, any] ? Rest : []

type Example4 = Init<[1, 2, 3, 4]>  // [1, 2, 3]
type Example5 = Init<[1]>           // []
type Example6 = Init<[]>            // []

// Получение первых N элементов
type Take<T extends any[], N extends number, Acc extends any[] = []> = 
  Acc['length'] extends N 
    ? Acc 
    : T extends [infer F, ...infer Rest]
      ? Take<Rest, N, [...Acc, F]>
      : Acc

type First3 = Take<[1, 2, 3, 4, 5], 3>  // [1, 2, 3]

// Пропуск первых N элементов
type Skip<T extends any[], N extends number> = 
  N extends 0 
    ? T 
    : T extends [any, ...infer Rest]
      ? Skip<Rest, Decrement<N>>
      : []

// Разделение кортежа на части
type Split<T extends any[], N extends number> = [Take<T, N>, Skip<T, N>]

type Parts = Split<[1, 2, 3, 4, 5], 2>  // [[1, 2], [3, 4, 5]]

Продвинутые паттерны типов

// Извлечение типов параметров функции
type Parameters<T> = T extends (...args: infer P) => any ? P : never

function example(a: string, b: number, c: boolean) {}
type Params = Parameters<typeof example>  // [string, number, boolean]

// Извлечение типа возвращаемого значения
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

function compute(): { result: number } {
  return { result: 42 }
}
type Result = ReturnType<typeof compute>  // { result: number }

// Конкатенация кортежей
type Concat<T extends any[], U extends any[]> = [...T, ...U]

type Combined = Concat<[1, 2], [3, 4]>  // [1, 2, 3, 4]

// Реверс кортежа
type Reverse<T extends any[]> = T extends [infer F, ...infer Rest]
  ? [...Reverse<Rest>, F]
  : []

type Reversed = Reverse<[1, 2, 3, 4]>  // [4, 3, 2, 1]

// Плоский массив из вложенных кортежей
type Flatten<T extends any[]> = T extends [infer F, ...infer Rest]
  ? F extends any[]
    ? [...Flatten<F>, ...Flatten<Rest>]
    : [F, ...Flatten<Rest>]
  : []

type Flat = Flatten<[1, [2, 3], [[4]], 5]>  // [1, 2, 3, 4, 5]

Практическое применение в реальных проектах

// Типизация функций с переменным числом аргументов
type EventCallback<Args extends any[]> = (...args: Args) => void

class EventEmitter<Events extends Record<string, any[]>> {
  on<K extends keyof Events>(
    event: K, 
    callback: EventCallback<Events[K]>
  ): void {
    // реализация
  }
  
  emit<K extends keyof Events>(
    event: K, 
    ...args: Events[K]
  ): void {
    // реализация
  }
}

// Использование
interface MyEvents {
  userLogin: [userId: string, timestamp: number]
  dataUpdate: [data: any]
  error: [code: number, message: string, details?: object]
}

const emitter = new EventEmitter<MyEvents>()

emitter.on('userLogin', (userId, timestamp) => {
  // userId: string, timestamp: number
})

emitter.emit('error', 404, 'Not Found', { url: '/api/users' })

// Типизация compose-функций
type Func = (arg: any) => any

type ComposeReturn<F extends Func[]> = 
  F extends [infer First extends Func, ...infer Rest extends Func[]]
    ? Rest extends []
      ? First
      : (arg: Parameters<First>[0]) => ComposeReturn<Rest> extends Func
        ? ReturnType<ComposeReturn<Rest>>
        : never
    : never

declare function compose<F extends Func[]>(...fns: F): ComposeReturn<F>

// Извлечение опциональных полей
type OptionalKeys<T> = {
  [K in keyof T]: T extends Record<K, T[K]> ? never : K
}[keyof T]

type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>

type SplitProps<T> = {
  required: Pick<T, RequiredKeys<T>>
  optional: Pick<T, OptionalKeys<T>>
}

Сравнительная таблица

КонтекстПозиция RESTПричина ограничения
Параметры функцийТолько последнийRuntime не знает количество аргументов заранее
ДеструктуризацияТолько последнийСинтаксическое правило, аналогичное параметрам
TypeScript типыЛюбая позицияCompile-time имеет полную информацию о структуре

Типичные ошибки

// ❌ Ошибка 1: REST не в конце параметров
function wrong(...items, callback) { }  // SyntaxError

// ✅ Правильно
function correct(callback, ...items) { }

// ❌ Ошибка 2: REST не в конце деструктуризации
const [first, ...middle, last] = array  // SyntaxError

// ✅ Правильно: используйте slice
const first = array[0]
const last = array[array.length - 1]
const middle = array.slice(1, -1)

// ❌ Ошибка 3: множественные REST
function invalid(...args1, ...args2) { }  // SyntaxError

// ❌ Ошибка 4: REST в деструктуризации объектов (это spread, не REST)
const { a, ...rest, b } = obj  // SyntaxError при попытке после REST

Когда использовать что

Параметры функций с REST - когда функция должна принимать переменное количество однотипных аргументов.

Деструктуризация с REST - когда нужно извлечь несколько первых элементов массива, а остальные собрать вместе.

TypeScript типы с REST - когда нужно создать utility-типы для работы со структурой кортежей и параметров функций.

Написать комментарий