Rest-параметры и тип void
Как работать с переменным числом аргументов через rest-оператор и когда использовать тип void для функций без возврата.
🎯 Что такое rest-параметры?
Rest-параметры (параметры-остатки) позволяют функции принимать любое количество аргументов и собирать их в массив. Это аналог rest-оператора из JavaScript, но с возможностью типизации.
💡 Аналогия: Представьте, что вы звоните в пиццерию и говорите: «Мне нужна пицца с чем угодно». Пиццерия собирает любые начинки, которые вы называете, в одну пиццу. Rest-параметры работают так же — собирают любое количество аргументов в один массив.
// Синтаксис: ...имя: тип[]
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
// Вызовы — можно передать сколько угодно аргументов
sum(1, 2); // 3
sum(1, 2, 3, 4, 5); // 15
sum(10); // 10
sum(); // 0 — пустой массив
// внутри функции numbers — это обычный массив
// sum(1, 2, 3) → numbers = [1, 2, 3]
Обратите внимание: rest-параметр ставится последним в списке параметров и всегда помечен тройным оператором ....
📝 Синтаксис rest-параметров
Давайте разберём синтаксис подробнее и рассмотрим разные варианты использования.
Базовый синтаксис
// rest-параметр: ...имя: тип[]
function logLevels(...levels: string[]): void {
console.log("Уровни логирования:", levels.join(" > "));
}
logLevels("error", "critical", "fatal");
// Выведет: Уровни логирования: error > critical > fatal
logLevels("info");
// Выведет: Уровни логирования: info
logLevels();
// Выведет: Уровни логирования: (пустая строка)
Rest-параметр с другими параметрами
// Обязательный параметр + rest
function createTag(tag: string, ...classes: string[]): string {
return `<${tag} class="${classes.join(" ")}">`;
}
createTag("div", "container", "main");
// ''
createTag("button", "btn", "btn-primary", "btn-lg");
// ''
// Несколько обязательных параметров + rest
function buildUrl(protocol: string, domain: string, ...path: string[]): string {
return `${protocol}://${domain}/${path.join("/")}`;
}
buildUrl("https", "example.com", "api", "users", "123");
// "https://example.com/api/users/123"
buildUrl("http", "localhost", "api", "v1");
// "http://localhost/api/v1"
Rest-параметр с необязательными параметрами
// Обязательный + необязательный + rest — правильный порядок
function formatMessage(
sender: string,
priority?: string,
...tags: string[]
): string {
const priorityPart = priority ? `[${priority}]` : "";
const tagsPart = tags.length > 0 ? ` (${tags.join(", ")})` : "";
return `${sender}: ${priorityPart}${tagsPart}`;
}
formatMessage("Анна");
// "Анна: "
formatMessage("Анна", "high");
// "Анна: [high]"
formatMessage("Анна", "high", "urgent", "backend");
// "Анна: [high] (urgent, backend)"
formatMessage("Анна", undefined, "info");
// "Анна: (info)"
🛠️ Rest-параметры в действии: реальные примеры
Rest-параметры находят применение во многих реальных сценариях. Рассмотрим наиболее типичные.
Пример 1: Математические функции
// Сумма любого количества чисел
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
// Среднее значение
function average(...numbers: number[]): number {
if (numbers.length === 0) return 0;
return sum(...numbers) / numbers.length;
}
// Максимум
function max(...numbers: number[]): number {
return Math.max(...numbers);
}
// Минимум
function min(...numbers: number[]): number {
return Math.min(...numbers);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(average(10, 20, 30)); // 20
console.log(max(5, 3, 9, 1, 7)); // 9
console.log(min(5, 3, 9, 1, 7)); // 1
Пример 2: Логирование
// Универсальная функция логирования
type LogLevel = "debug" | "info" | "warn" | "error";
function log(level: LogLevel, ...messages: unknown[]): void {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
console.log(prefix, ...messages);
}
// Использование
log("info", "Сервер запущен");
log("debug", "Переменная x =", 42, "тип:", typeof 42);
log("warn", "Мало памяти:", { used: "80%", free: "20%" });
log("error", "Критическая ошибка:", new Error("Something went wrong"));
Пример 3: Валидация
// Проверка, что все значения удовлетворяют условию
function allMatch(predicate: (item: T) => boolean, ...items: T[]): boolean {
return items.every(predicate);
}
// Использование
allMatch(n => n > 0, 1, 2, 3, 4, 5); // true — все положительные
allMatch(n => n > 0, 1, -2, 3, 4, 5); // false — -2 не положительное
allMatch(s => s.length > 0, "hello", "world"); // true — все непустые
allMatch(s => s.length > 0, "hello", ""); // false — "" пустая
// Проверка, что хотя бы одно значение удовлетворяет условию
function anyMatch(predicate: (item: T) => boolean, ...items: T[]): boolean {
return items.some(predicate);
}
anyMatch(n => n > 10, 1, 2, 3, 15, 5); // true — 15 > 10
anyMatch(n => n > 10, 1, 2, 3, 4, 5); // false — нет чисел > 10
Пример 4: Фабрика объектов
// Создание объекта из пар ключ-значение
function createObject(
...pairs: [K, V][]
): Record {
return Object.fromEntries(pairs) as Record;
}
// Использование
const config = createObject(
["host", "localhost"],
["port", 3000],
["debug", true]
);
// { host: "localhost", port: 3000, debug: true }
// Тип: Record<"host" | "port" | "debug", string | number | boolean>
🔄 Spread vs Rest: в чём разница?
Оператор ... используется в двух контекстах: в параметрах функции (rest) и при вызове функции (spread). Они делают противоположные вещи.
// REST: собирает аргументы в массив
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
// sum(1, 2, 3) → numbers = [1, 2, 3] — собирает
// SPREAD: раскрывает массив в отдельные аргументы
const nums = [1, 2, 3];
sum(...nums); // → sum(1, 2, 3) — раскрывает
Визуально
| Оператор | Где используется | Что делает | Пример |
|---|---|---|---|
| Rest | Параметры функции | Собирает аргументы в массив | function f(...args: number[]) {} |
| Spread | Вызов функции / литералы | Раскрывает массив/объект | f(...[1, 2, 3]) |
// Пример: rest собирает, spread раскрывает
function merge(...arrays: T[][]): T[] {
// REST: arrays = [[1,2], [3,4], [5,6]]
return arrays.reduce((merged, arr) => [...merged, ...arr], []);
// ↑ SPREAD: раскрывает merged
// ↑ SPREAD: раскрывает arr
}
merge([1, 2], [3, 4], [5, 6]); // [1, 2, 3, 4, 5, 6]
📭 Тип void: глубокое погружение
Тип void используется для функций, которые не возвращают значения. Давайте разберём его подробнее и поймём все нюансы.
void в объявлении функции
// Функция ничего не возвращает
function logEvent(event: string): void {
console.log(`[${new Date().toISOString()}] ${event}`);
}
// Функция с побочным эффектом
function sendAnalytics(event: string, data: Record): void {
fetch("/api/analytics", {
method: "POST",
body: JSON.stringify({ event, data })
});
}
// Функцияизменить интерфейс
function updateUser(user: { name: string; age: number }, newAge: number): void {
user.age = newAge;
}
Что можно и нельзя делать с void
// ✅ Можно — ничего не возвращать
function doA(): void {
// Неявный return — OK
}
// ✅ Можно — использовать return без значения
function doB(): void {
console.log("doing something");
return; // Явный return без значения — OK
}
// ❌ Нельзя — возвращать значение
function doC(): void {
return 42; // Ошибка: A 'void' function must not return a value
}
// ❌ Нельзя — использовать результат
function doD(): void {
console.log("hello");
}
const result = doD();
console.log(result); // undefined — результат void-функции всегда undefined
void как тип аргумента callback
Есть важный нюанс: функция с типом возврата void может возвращать значение — TypeScript просто игнорирует его. Это важно при передаче callback-функций:
// Функция, принимающая callback с типом void
function forEach(array: T[], callback: (item: T) => void): void {
for (const item of array) {
callback(item);
}
}
// ✅ Callback возвращает void — всё ок
forEach([1, 2, 3], (n) => {
console.log(n);
});
// ✅ Callback возвращает значение — тоже ок!
// TypeScript не будет ругаться, потому что результат игнорируется
forEach([1, 2, 3], (n) => n * 2); // Возвращает число, но forEach его не использует
// Это отличие от обычного типизирования:
function apply(value: T, fn: (item: T) => R): R {
return fn(value);
}
apply(5, (n) => n * 2); // ✅ OK — fn возвращает number
apply(5, (n) => { console.log(n); }); // ⚠️ Проблема: fn возвращает void, но apply ожидает R
⚠️ Важный нюанс: Тип void как тип callback-а означает «результат может быть, но будет проигнорирован». Это не запрещает возвращать значение — TypeScript просто не будет его использовать.
⚖️ void vs undefined vs never
Эти три типа часто путают. Давайте раз и навсегда разберём разницу.
| Тип | Значение | Когда использовать |
|---|---|---|
| void | Функция не возвращает значения | Побочные эффекты, логирование, модификация |
| undefined | Функция возвращает именно undefined | Когда undefined — осмысленный результат |
| never | Функция никогда не завершается | Ошибки, бесконечные циклы, exhaustiveness |
// void — не возвращает значения (побочный эффект)
function save(data: unknown): void {
database.write(data);
}
// undefined — возвращает undefined (осмысленный результат)
function find(id: string): User | undefined {
return users.find(u => u.id === id);
// undefined означает «пользователь не найден»
}
// never — никогда не завершается
function throwError(msg: string): never {
throw new Error(msg);
}
// Сравнение на практике
function processValue(value: string): void {
console.log(value); // void — ничего не возвращаем
}
function getValue(key: string): string | undefined {
return store[key]; // undefined возможен — ключ может отсутствовать
}
function fatal(message: string): never {
alert(message);
throw new Error(message); // never — выполнение прерывается
📦 Rest-параметры и кортежи
Rest-параметры могут иметь тип кортежа (tuple). Это позволяет задать точные типы для первых элементов, а оставшиеся — собрать через rest.
// Определяем тип кортежа
type LogEntry = [number, string, LogLevel];
// Rest-параметр с типом кортежа
function logEntries(...entries: LogEntry[]): void {
for (const [timestamp, message, level] of entries) {
console.log(`[${timestamp}] [${level}] ${message}`);
}
}
// Каждый аргумент должен соответствовать типу кортежа
logEntries(
[Date.now(), "Сервер запущен", "info"],
[Date.now(), "Мало памяти", "warn"],
[Date.now(), "Ошибка подключения", "error"]
);
// Пример: конфигурация с фиксированными и переменными параметрами
type Route = [string, "GET" | "POST" | "PUT" | "DELETE", ...string[]];
function registerRoutes(...routes: Route[]): void {
for (const [path, method, ...middleware] of routes) {
console.log(`${method} ${path} ${middleware.length > 0 ? `[${middleware.join(", ")}]` : ""}`);
}
}
registerRoutes(
["/api/users", "GET", "auth", "cache"],
["/api/users", "POST", "auth"],
["/api/users/:id", "DELETE", "auth", "admin"]
);
🏗️ Практические паттерны с rest и void
Паттерн 1: curry (частичное применение)
// Функция для создания функции с зафиксированными первыми аргументами
function curry(
fn: (...args: A) => R,
...fixedArgs: A extends [infer First, ...infer Rest] ? [First] : never[]
) {
return (...restArgs: A extends [unknown, ...infer Rest] ? Rest : never[]): R => {
return fn(...fixedArgs, ...restArgs) as R;
};
}
// Использование
function add(a: number, b: number): number {
return a + b;
}
const add5 = curry(add, 5);
add5(3); // 8
add5(10); // 15
Паттерн 2: Event Emitter
// Простой event emitter с типизированными событиями
type EventMap = {
login: [userId: string];
logout: [userId: string];
message: [from: string, text: string];
error: [code: number, message: string];
};
class TypedEventEmitter {
private listeners: Map = new Map();
on(
event: K,
...callbacks: Array<(...args: EventMap[K]) => void>
): void {
for (const callback of callbacks) {
if (!this.listeners.has(event as string)) {
this.listeners.set(event as string, []);
}
this.listeners.get(event as string)!.push(callback);
}
}
emit(event: K, ...args: EventMap[K]): void {
const callbacks = this.listeners.get(event as string) || [];
for (const callback of callbacks) {
callback(...args);
}
}
}
// Использование
const emitter = new TypedEventEmitter();
emitter.on("login", (userId) => {
console.log("Пользователь вошёл:", userId);
});
emitter.on("message", (from, text) => {
console.log(`Сообщение от ${from}: ${text}`);
});
emitter.emit("login", "user123");
emitter.emit("message", "Анна", "Привет!");
Паттерн 3: Pipeline (конвейер)
// Применение цепочки функций к значению
function pipe(value: T, ...fns: Array<(arg: T) => T>): T {
return fns.reduce((acc, fn) => fn(acc), value);
}
// Использование
const result = pipe(
5,
(n) => n * 2, // 10
(n) => n + 3, // 13
(n) => n - 1 // 12
);
console.log(result); // 12
// С дженериками для разных типов
function pipeWith(value: A, fn1: (arg: A) => B): B;
function pipeWith(value: A, fn1: (arg: A) => B, fn2: (arg: B) => C): C;
function pipeWith(
value: A,
fn1: (arg: A) => B,
fn2: (arg: B) => C,
fn3: (arg: C) => D
): D;
function pipeWith(value: unknown, ...fns: Function[]): unknown {
return fns.reduce((acc, fn) => fn(acc), value);
}
// Строка → число → строка
const formatted = pipeWith(
" 42 ",
(s: string) => s.trim(), // "42"
(s: string) => Number(s), // 42
(n: number) => n * 2 // 84
);
console.log(formatted); // 84
❌ Частые ошибки
Ошибка 1: Rest-параметр не в конце
// ❌ Ошибка — rest-параметр должен быть последним
function bad(...args: number[], name: string) {}
// Error: A rest parameter must be last in a parameter list.
// ✅ Правильно — rest всегда последний
function good(name: string, ...args: number[]) {}
Ошибка 2: Попытка вернуть значение из void-функции
// ❌ Ошибка
function process(data: unknown): void {
return data; // Error: A 'void' function must not return a value
}
// ✅ Правильно
function process(data: unknown): void {
console.log(data);
// return; — необязательно, но можно
}
Ошибка 3: Использование результата void-функции
function log(message: string): void {
console.log(message);
}
// ❌ Плохо — результат void-функции всегда undefined
const result = log("hello");
result.toUpperCase(); // TypeError: Cannot read property 'toUpperCase' of undefined
// ✅ Правильно — void-функции не используют результат
log("hello");
// Не сохраняем результат в переменную
Ошибка 4: Неправильная типизация rest-параметра
// ❌ Плохо — без типизации rest-параметр будет any[]
function sum(...args) {
return args.reduce((a, b) => a + b, 0);
}
// ✅ Правильно — ясно указываем тип
function sum(...args: number[]): number {
return args.reduce((a, b) => a + b, 0);
}
// ✅ Также можно указать union-тип
function logValues(...values: (string | number)[]): void {
console.log(values.join(" "));
}
📋 Итоги урока
- Rest-параметры (
...args: T[]) — собирают переменное число аргументов в массив - Rest всегда стоит последним в списке параметров
- Spread (
...array) — раскрывает массив в отдельные аргументы при вызове - void — функция не возвращает значения (побочные эффекты)
- void vs undefined — void = намерение не возвращать, undefined = возвращает undefined
- void vs never — void завершается без результата, never не завершается вообще
✅ Ключевое правило: Rest-параметры дают гибкость — функция принимает любое количество аргументов. Void чётко говорит: «я ничего не возвращаю — не пытайтесь использовать мой результат».
В следующем уроке мы разберём перегрузку функций — как одна функция может работать с разными типами аргументов.
Проверяем понимание
6 вопросов