Avatar ·

Перегрузка конструктора в TypeScript: как правильно объявить несколько вариантов?

Привет! Я только начал изучать TypeScript и столкнулся с проблемой перегрузки конструктора. В документации сказано, что TypeScript поддерживает перегрузку функций, но я не могу заставить это работать для конструктора класса. У меня есть класс `User`, который должен принимать разные наборы параметров: - Имя и возраст (обязательно) - Только имя (возраст по умолчанию 0) - Объект с полями name и age Вот что я пытаюсь сделать: ```typescript class User { name: string; age: number; constructor(name: string, age: number); constructor(name: string); constructor(obj: {name: string, age: number}) { this.name = name; this.age = age; } } ``` TypeScript ругается: "Type '{name: string, age: number}' is not assignable to type 'string'". Я понимаю, что проблема в том, что все сигнатуры должны быть совместимы, но как правильно реализовать перегрузку конструктора? Может быть, есть другой способ сделать это? Буду благодарен за подробное объяснение с примерами. <<>>

Правильная реализация перегрузки конструктора в TypeScript

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

class User {
  name: string;
  age: number;

  // Сигнатуры перегрузки (только объявления)
  constructor(name: string, age: number);
  constructor(name: string);
  constructor(obj: { name: string; age: number });

  // Реализация с объединенным типом параметров
  constructor(param1: string | { name: string; age: number }, param2?: number) {
    if (typeof param1 === 'string') {
      this.name = param1;
      this.age = param2 ?? 0; // если age не передан, ставим 0
    } else {
      this.name = param1.name;
      this.age = param1.age;
    }
  }
}

// Примеры использования:
const user1 = new User('Alice', 30);     // name: 'Alice', age: 30
const user2 = new User('Bob');           // name: 'Bob', age: 0
const user3 = new User({ name: 'Charlie', age: 25 }); // name: 'Charlie', age: 25

Ключевые моменты:

  • Сигнатуры перегрузки — это только объявления (без тела). Они определяют возможные варианты вызова.
  • Реализация должна быть одна и принимать наиболее общий тип (объединение всех вариантов).
  • Параметры, которые есть не во всех сигнатурах, делаются опциональными (?) или со значением по умолчанию.
  • Внутри реализации нужно проверять типы параметров и выполнять соответствующую логику.

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

TypeScript проверяет сигнатуры перегрузки при вызове конструктора, но во время выполнения используется только одна реализация. Поэтому внутри реализации мы должны сами обработать все варианты. Использование typeof и проверки на undefined — стандартный подход.

Более сложный пример с защитниками типов:

class Rectangle {
  width: number;
  height: number;

  constructor(width: number, height: number);
  constructor(size: number); // квадрат
  constructor(dimensions: { width: number; height: number });

  constructor(first: number | { width: number; height: number }, second?: number) {
    if (typeof first === 'number' && second !== undefined) {
      // Вариант: (width, height)
      this.width = first;
      this.height = second;
    } else if (typeof first === 'number') {
      // Вариант: (size) — квадрат
      this.width = first;
      this.height = first;
    } else {
      // Вариант: ({ width, height })
      this.width = first.width;
      this.height = first.height;
    }
  }
}

Альтернативный подход с фабричными методами

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

class User {
  private constructor(public name: string, public age: number) {}

  static withName(name: string): User {
    return new User(name, 0);
  }

  static withNameAndAge(name: string, age: number): User {
    return new User(name, age);
  }

  static fromObject(obj: { name: string; age: number }): User {
    return new User(obj.name, obj.age);
  }
}

const user = User.withName('Alice');

Этот подход делает код более читаемым и избавляет от сложных проверок типов.

Итог

Перегрузка конструктора в TypeScript работает, но требует явного объявления всех сигнатур и одной реализации, которая обрабатывает все случаи. Главное — помнить о порядке: сначала сигнатуры, потом реализация. Если вариантов много, рассмотрите фабричные методы или паттерн "Строитель".

<<>>

Использование опциональных параметров и значений по умолчанию

Вместо перегрузки конструктора можно часто обойтись одним конструктором с опциональными параметрами и значениями по умолчанию. Это проще и часто более читаемо:

class User {
  name: string;
  age: number;

  constructor(name: string, age: number = 0) {
    this.name = name;
    this.age = age;
  }
}

// Использование:
const user1 = new User('Alice', 30);  // age = 30
const user2 = new User('Bob');        // age = 0

Этот подход покрывает первые два варианта из вашего вопроса. Для третьего варианта (прием объекта) можно добавить перегрузку, но если нужно именно три варианта, лучше использовать объединение типов:

class User {
  name: string;
  age: number;

  constructor(param: string | { name: string; age: number }, age?: number) {
    if (typeof param === 'string') {
      this.name = param;
      this.age = age ?? 0;
    } else {
      this.name = param.name;
      this.age = param.age;
    }
  }
}

Такой подход избавляет от необходимости писать несколько сигнатур перегрузки, а вся логика остается в одном конструкторе. TypeScript сам выведет типы при вызове.