Avatar ·

Как задать значение по умолчанию для объекта-параметра функции в TypeScript?

Привет! Я только начал изучать TypeScript, и у меня возникла проблема с параметрами функций, которые являются объектами. Допустим, у меня есть функция, которая принимает объект с настройками: ```typescript function createUser(config: { name: string; age: number; isAdmin: boolean }) { // ... } ``` Я хочу, чтобы параметр `config` был необязательным, и если его не передали, то использовались значения по умолчанию. В JavaScript я бы сделал так: ```javascript function createUser(config = { name: 'Guest', age: 18, isAdmin: false }) { // ... } ``` Но в TypeScript это не работает, потому что тип `config` должен быть определён. Я пробовал разные варианты: 1. Просто указать значение по умолчанию — TypeScript ругается на несоответствие типов. 2. Использовать `Partial` — но тогда я не могу задать конкретные значения по умолчанию. 3. Сделать все поля необязательными через `?` — но тогда теряется строгость типов. Вот пример моего кода, который не компилируется: ```typescript interface UserConfig { name: string; age: number; isAdmin: boolean; } function createUser(config: UserConfig = { name: 'Guest', age: 18, isAdmin: false }) { console.log(config.name, config.age, config.isAdmin); } // Ошибка: Type '{ name: string; age: number; isAdmin: false; }' is not assignable to type 'UserConfig'. ``` Как правильно решить эту проблему? Нужно ли создавать отдельный тип для дефолтных значений или есть какой-то более элегантный способ? Заранее спасибо за помощь! <<>>

Основная проблема и её решение

Вы столкнулись с классической ситуацией, когда TypeScript не позволяет присвоить литерал объекта типу, если в этом литерале не указаны все обязательные поля. Дело в том, что TypeScript проверяет избыточность свойств (excess property checks) при прямом присваивании литерала. Однако ваш код корректен по логике — вы просто хотите задать значения по умолчанию.

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

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

// Дефолтные значения
const defaultConfig: UserConfig = {
  name: 'Guest',
  age: 18,
  isAdmin: false,
};

function createUser(config: Partial<UserConfig> = {}) {
  // Объединяем переданные значения с дефолтными
  const finalConfig: UserConfig = { ...defaultConfig, ...config };
  console.log(finalConfig.name, finalConfig.age, finalConfig.isAdmin);
}

// Использование:
createUser(); // Guest, 18, false
createUser({ name: 'Alice' }); // Alice, 18, false
createUser({ name: 'Bob', age: 25 }); // Bob, 25, false
createUser({ isAdmin: true }); // Guest, 18, true

Почему это работает?

  • Partial<UserConfig> делает все поля необязательными: { name?: string; age?: number; isAdmin?: boolean }.
  • Значение по умолчанию {} — пустой объект, который совместим с Partial.
  • Оператор ... (spread) объединяет переданные свойства с дефолтными, причём переданные значения перезаписывают дефолтные.
  • Итоговый объект finalConfig гарантированно имеет тип UserConfig — все поля обязательны и имеют корректные значения.

Более строгий вариант с дженериками

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

function createUser<T extends Partial<UserConfig>>(config: T = {} as T) {
  const finalConfig: UserConfig = { ...defaultConfig, ...config };
  // ...
}

// Ошибка! Свойства 'namee' не существует в UserConfig
createUser({ namee: 'Test' }); 

Альтернатива — использовать Required

Можно также сделать все поля в интерфейсе необязательными, а внутри функции использовать Required для итогового типа:

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

const defaultConfig: Required<UserConfig> = {
  name: 'Guest',
  age: 18,
  isAdmin: false,
};

function createUser(config: UserConfig = {}) {
  const finalConfig: Required<UserConfig> = { ...defaultConfig, ...config };
  // ...
}

Но этот вариант менее удобен, так как интерфейс теряет информацию об обязательности полей при вызове функции.

Рекомендация

Используйте первый вариант с Partial и спред-оператором. Он:

  • Чётко разделяет обязательные и необязательные поля в интерфейсе.
  • Не требует дублирования дефолтных значений в сигнатуре функции.
  • Хорошо масштабируется — можно легко добавить новые поля.
  • Полностью типизирован на всех этапах.

Единственный минус — вы теряете проверку на избыточные свойства при передаче объекта. Но это решается дженериком, как показано выше.

<<>>

Использование фабричной функции или класса

Если вам часто нужно создавать объекты с дефолтными значениями, можно вынести логику в отдельную сущность. Это особенно удобно, когда дефолты сложные или их много.

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

class UserConfigBuilder {
  private config: UserConfig = {
    name: 'Guest',
    age: 18,
    isAdmin: false,
  };

  withName(name: string): this {
    this.config.name = name;
    return this;
  }

  withAge(age: number): this {
    this.config.age = age;
    return this;
  }

  withAdmin(isAdmin: boolean): this {
    this.config.isAdmin = isAdmin;
    return this;
  }

  build(): UserConfig {
    return { ...this.config };
  }
}

function createUser(config?: Partial<UserConfig>) {
  const finalConfig: UserConfig = config 
    ? { ...new UserConfigBuilder().build(), ...config }
    : new UserConfigBuilder().build();
  // ...
}

Этот подход даёт читаемый fluent-интерфейс и строгую типизацию. Однако он избыточен для простых случаев.

Использование утилитарного типа Defaults

Можно создать вспомогательный тип, который автоматически подставляет дефолты:

type WithDefaults<T, D extends Partial<T>> = Omit<T, keyof D> & Partial<D>;

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

type UserConfigInput = WithDefaults<UserConfig, { name: string; age: number }>;
// Результат: { name: string; age: number; isAdmin?: boolean }

function createUser(config: UserConfigInput = { name: 'Guest', age: 18 }) {
  const finalConfig: UserConfig = { 
    isAdmin: false, // дефолт для isAdmin
    ...config 
  };
  // ...
}

Этот вариант даёт тонкий контроль над тем, какие поля обязательны при вызове, а какие имеют дефолты. Но требует дополнительного типа и может запутать новичков.

Вывод: Для 90% случаев достаточно первого ответа с Partial и спредом. Остальные варианты — для специфических требований проекта.