$ sudo teach IT
МОДУЛЬ 4 · УРОК 3

Rest-параметры и тип void

Как работать с переменным числом аргументов через rest-оператор и когда использовать тип void для функций без возврата.

⏱ ~20 минут 🎓 Базовый уровень 📌 Функции

🎯 Что такое 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 вопросов

Сумма с rest-параметром

Premium