Как задать значение по умолчанию для объекта TypeScript в аргументе: полный гайд

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

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

Начать курс

Введение: почему это так часто ищут?

Каждый разработчик, который переходит с JavaScript на TypeScript или просто пишет на этом языке, рано или поздно сталкивается с проблемой: как правильно задать значение по умолчанию для объекта, который передается в функцию? На Stack Overflow этот вопрос набрал тысячи просмотров и десятки ответов. Почему? Потому что, казалось бы, простая задача — "если объект не передан, используй пустой объект" — в TypeScript превращается в целый квест с типами, интерфейсами и деструктуризацией.

В этой статье мы разберем все возможные способы, от самых простых до продвинутых, с примерами кода и пояснениями. Вы узнаете, как настроить значения по умолчанию для объекта TypeScript, переданного в качестве аргумента, и избежать типичных ошибок. Поехали!

Проблема: что не так с обычным подходом?

Допустим, у нас есть функция, которая принимает объект с настройками:

function createUser(config: { name?: string; age?: number }) {
  // ...
}

createUser(); // Ошибка! Нужен аргумент

В JavaScript мы могли бы просто написать function createUser(config = {}) и дело в шляпе. Но в TypeScript такой трюк не пройдет: компилятор будет ругаться на несоответствие типов. Почему? Потому что пустой объект {} не соответствует интерфейсу { name?: string; age?: number } — в нем просто нет этих свойств.

И тут начинается самое интересное. Давайте разберем все способы решения.

Способ 1: Деструктуризация с присвоением значений по умолчанию

Самый элегантный и популярный метод — использовать деструктуризацию прямо в параметрах функции. Вы извлекаете свойства объекта и сразу задаете им значения по умолчанию:

function createUser({ name = "Гость", age = 18 }: { name?: string; age?: number } = {}) {
  console.log(`Создан пользователь: ${name}, возраст: ${age}`);
}

createUser(); // "Создан пользователь: Гость, возраст: 18"
createUser({ name: "Анна" }); // "Создан пользователь: Анна, возраст: 18"
createUser({ age: 25 }); // "Создан пользователь: Гость, возраст: 25"

Обратите внимание на = {} в конце. Это значение по умолчанию для самого объекта. Если вы не передадите аргумент, TypeScript использует пустой объект, а деструктуризация подставит значения по умолчанию для каждого свойства.

Плюсы: чисто, лаконично, работает как часы.
Минусы: нужно явно указывать тип для каждого свойства.

Способ 2: Использование интерфейса с Partial

Если ваш объект большой, описывать все типы в параметрах — моветон. Выносим интерфейс отдельно и используем Partial<T>:

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

function createUser(config: Partial<UserConfig> = {}) {
  const defaults: UserConfig = {
    name: "Гость",
    age: 18,
    email: "noemail@example.com",
    isAdmin: false
  };
  
  const mergedConfig = { ...defaults, ...config };
  console.log(mergedConfig);
}

createUser({ name: "Иван" });
// { name: "Иван", age: 18, email: "noemail@example.com", isAdmin: false }

Здесь Partial<UserConfig> делает все свойства необязательными. Внутри функции мы сливаем дефолтные значения с переданными. Работает отлично, но требует ручного слияния.

Способ 3: Классический подход с проверкой

Иногда нужно больше контроля. Например, если вы хотите задать значение по умолчанию только для undefined, но не для null. Используем старый добрый if:

function createUser(config?: UserConfig) {
  const name = config?.name ?? "Гость";
  const age = config?.age ?? 18;
  const email = config?.email ?? "noemail@example.com";
  
  console.log({ name, age, email });
}

createUser(); // { name: "Гость", age: 18, email: "noemail@example.com" }
createUser({ name: "Мария", age: null as any }); // { name: "Мария", age: 18, email: "noemail@example.com" }

Оператор ?? (nullish coalescing) работает только с null и undefined. Если передать age: 0, он не подставит дефолт. Это бывает полезно.

Способ 4: Фабрика значений по умолчанию

Для сложных объектов, которые нужно создавать динамически (например, с метками времени), используйте фабричную функцию:

interface UserConfig {
  name: string;
  age: number;
  createdAt: Date;
}

function getDefaultUserConfig(): UserConfig {
  return {
    name: "Гость",
    age: 18,
    createdAt: new Date()
  };
}

function createUser(config: Partial<UserConfig> = {}) {
  const merged = { ...getDefaultUserConfig(), ...config };
  console.log(merged);
}

createUser({ name: "Ольга" });
// { name: "Ольга", age: 18, createdAt: 2025-01-15T10:30:00.000Z }

Каждый вызов getDefaultUserConfig() создает новый объект, поэтому createdAt будет актуальным. Это важно, если вы не хотите, чтобы все пользователи получили одно и то же время создания.

Способ 5: Глубокое слияние для вложенных объектов

А что если у вас объект внутри объекта? Простой spread-оператор не подойдет — он сделает поверхностное копирование. Нужно рекурсивное слияние:

interface AppConfig {
  theme: {
    color: string;
    fontSize: number;
  };
  language: string;
}

function deepMerge<T>(target: T, source: Partial<T>): T {
  const output = { ...target };
  for (const key in source) {
    if (source[key] instanceof Object && key in target) {
      output[key] = deepMerge(target[key], source[key]);
    } else {
      output[key] = source[key] as any;
    }
  }
  return output;
}

function setupApp(config: Partial<AppConfig> = {}) {
  const defaults: AppConfig = {
    theme: { color: "blue", fontSize: 14 },
    language: "ru"
  };
  
  const merged = deepMerge(defaults, config);
  console.log(merged);
}

setupApp({ theme: { color: "red" } });
// { theme: { color: "red", fontSize: 14 }, language: "ru" }

Внимание: для production-кода лучше использовать готовые библиотеки вроде Lodash _.merge, но для понимания сути — отличный пример.

Типичные ошибки и как их избежать