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-типы для работы со структурой кортежей и параметров функций.