TypeScript: передаем одни опциональные параметры, пропуская другие

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Проклятие пропущенных параметров в TypeScript

Вы когда-нибудь писали функцию с десятью опциональными параметрами, а потом пытались передать только пятый и десятый? И тут же получали ошибку компиляции или, что ещё хуже, тихую логическую ошибку? Знакомо?

На Stack Overflow этот вопрос собрал тысячи просмотров. Разработчики на TypeScript постоянно спотыкаются об одну и ту же граблю: как передать один опциональный параметр, пропустив другой? Ведь стандартный синтаксис этого не позволяет – параметры передаются по позиции, а не по имени.

Сегодня мы раз и навсегда разберёмся с этой болью. Я покажу вам 4 рабочих способа решить проблему: от простого рефакторинга до продвинутых техник с partial-объектами. Поехали!

Почему это вообще проблема? Позиционное проклятие

В TypeScript (как и в JavaScript) параметры функции передаются по порядку. Если у вас есть функция:

function createUser(
  name: string,
  age?: number,
  email?: string,
  isAdmin?: boolean
) { /* ... */ }

Вы не можете вызвать её так: createUser('John', , 'john@mail.com'). Пропуск параметра оставляет дыру, которую TypeScript не переварит. Придётся передавать undefined или null, что загрязняет код и логику.

Но есть и хорошая новость: мы можем обойти это ограничение несколькими элегантными способами. Давайте разберём каждый.

Способ 1: Объект параметров (самый правильный)

Это рекомендуемый паттерн в серьёзных проектах. Вместо кучи позиционных параметров используем один объект с опциональными полями:

interface UserOptions {
  name: string;
  age?: number;
  email?: string;
  isAdmin?: boolean;
}

function createUser(options: UserOptions): void {
  const { name, age, email, isAdmin } = options;
  console.log(`Создан пользователь ${name}, возраст: ${age ?? 'не указан'}`);
}

// Теперь можно передавать только нужное!
createUser({ name: 'Анна', email: 'anna@mail.com' });
createUser({ name: 'Петр', isAdmin: true });

Преимущества очевидны:

  • Неважно, сколько параметров – передаём только то, что нужно
  • Код самодокументируемый: сразу видно, что за что отвечает
  • Легко добавлять новые поля без изменения сигнатуры

Этот подход решает 90% проблем с опциональными параметрами. Но если вы работаете с легаси или не можете изменить API – читайте дальше.

Способ 2: Перегрузка функций (function overloading)

TypeScript позволяет объявить несколько сигнатур для одной функции. Это удобно, когда у вас есть конкретные комбинации параметров:

// Объявляем перегрузки
function getData(url: string): Promise<any>;
function getData(url: string, timeout: number): Promise<any>;
function getData(url: string, timeout: number, retries: number): Promise<any>;

// Реализация (должна быть самой общей)
function getData(
  url: string,
  timeout?: number,
  retries?: number
): Promise<any> {
  // логика
  return fetch(url, { signal: AbortSignal.timeout(timeout ?? 5000) });
}

// Теперь можно вызывать так:
getData('/api/users');               // timeout = 5000 (по умолчанию)
getData('/api/users', 3000);         // timeout = 3000
// А вот так – нельзя (пропуск timeout недопустим без перегрузки):
// getData('/api/users', undefined, 2); // плохо

Минус: перегрузки не решают проблему пропуска серединных параметров. Они лишь дают строгие комбинации. Для гибкой передачи лучше использовать первый способ.

Способ 3: Rest параметры + деструктуризация (экзотика)

Если вы хотите сохранить позиционность, но добавить гибкости, можно использовать кортежи с rest:

type OptionalParams = [number, string?, boolean?];

function processData(
  required: string,
  ...optional: OptionalParams
): void {
  const [id, label, flag] = optional;
  console.log(required, id, label ?? 'default', flag ?? false);
}

// Вызов: передаём только id и flag
processData('action', 42, undefined, true);
// Всё ещё нужно передавать undefined для label...

Честно говоря, этот способ не решает проблему полностью – вы всё ещё передаёте undefined. Но он даёт строгую типизацию для кортежа. Используйте его только если очень хочется сохранить позиционный синтаксис.

Способ 4: Утилитарный тип Partial<T> (продвинутый)

Когда вы работаете с большими конфигурационными объектами, можно сделать все поля опциональными, кроме обязательных. Но как пропустить одно поле, а другое передать? На помощь приходит Partial:

interface Config {
  host: string;
  port: number;
  timeout: number;
  retries: number;
}

function setupServer(config: Partial<Config> & { host: string }): void {
  const defaults: Config = {
    host: 'localhost',
    port: 8080,
    timeout: 5000,
    retries: 3
  };
  const merged = { ...defaults, ...config };
  console.log(`Сервер запущен на ${merged.host}:${merged.port}`);
}

// Теперь можем передать только host и timeout, пропустив port и retries
setupServer({ host: 'example.com', timeout: 10000 });

Это мощная техника для функций с большим количеством настроек. Вы определяете полный интерфейс, а потом делаете его частично опциональным через пересечение типов.

Сравнение подходов: что выбрать?

Давайте сведём всё в наглядную таблицу (мысленно, но вы запомните):

  • Объект параметров – универсальное решение, подходит для 80% случаев. Используйте его в новом коде.
  • Перегрузки – для строгих API с фиксированными комбинациями (например, библиотечные функции).
  • Rest + кортежи – редко, только если нужно сохранить позиционность и строгую типизацию.
  • Partial<T> – для конфигурационных объектов с большим числом полей.

Живой пример из реального проекта

Представьте, что вы пишете функцию для отправки уведомлений. У неё есть обязательный параметр (сообщение) и куча опциональных: канал, приоритет, задержка, повторные попытки. Вот как это будет выглядеть с объектом параметров:

interface NotificationOptions {
  message: string;
  channel?: 'email' | 'sms' | 'push';
  priority?: 'low' | 'normal' | 'high';
  delay?: number;        // в миллисекундах
  retries?: number;
}

function sendNotification(options: NotificationOptions): Promise<void> {
  const { message, channel = 'email', priority = 'normal', delay = 0, retries = 0 } = options;
  // ... реализация отправки
  console.log(`[${priority.toUpperCase()}] Отправка через ${channel                

Блоги

Рекомендаций по книгам