Перегрузка конструктора в TypeScript: как правильно реализовать?
Правильная реализация перегрузки конструктора в TypeScript
В TypeScript, в отличие от C++ или Java, нет синтаксической поддержки множественных конструкторов. Однако это можно элегантно реализовать с помощью перегрузки сигнатур (overload signatures) и опциональных параметров. Вот как это делается:
class User {
// 1. Объявляем сигнатуры перегрузки (без реализации)
constructor(email: string);
constructor(name: string, age: number);
constructor();
// 2. Реализация с универсальными параметрами
constructor(param1?: string, param2?: number) {
if (typeof param1 === 'string' && param2 === undefined) {
// Случай 1: только email
this.email = param1;
this.name = 'Unknown';
this.age = 0;
} else if (typeof param1 === 'string' && typeof param2 === 'number') {
// Случай 2: имя и возраст
this.name = param1;
this.age = param2;
this.email = `${param1.toLowerCase()}@example.com`;
} else {
// Случай 3: пустой конструктор (значения по умолчанию)
this.email = 'default@example.com';
this.name = 'Guest';
this.age = 18;
}
}
// Свойства класса
email: string;
name: string;
age: number;
}
Как это работает?
- Сигнатуры перегрузки — это объявления без тела, которые сообщают TypeScript, какие варианты вызова допустимы. Они пишутся перед реальной реализацией.
- Реализация — один конструктор с опциональными параметрами (
param1?: string, param2?: number). Внутри него мы проверяем типы и количество аргументов, чтобы определить, какой вариант вызван. - TypeScript сам проверяет, что все сигнатуры совместимы с реализацией. Если вы попытаетесь вызвать конструктор с несовместимыми типами (например,
new User(42)), компилятор выдаст ошибку.
Полный пример с использованием
const user1 = new User("alice@mail.com");
console.log(user1.email); // "alice@mail.com"
console.log(user1.name); // "Unknown"
const user2 = new User("Bob", 30);
console.log(user2.name); // "Bob"
console.log(user2.age); // 30
const user3 = new User();
console.log(user3.email); // "default@example.com"
// Ошибка компиляции:
// const user4 = new User(123); // Argument of type 'number' is not assignable to parameter of type 'string'.
Альтернативный подход: фабричные методы
Если перегрузка становится слишком сложной (например, больше 3 вариантов), лучше использовать статические фабричные методы:
class User {
private constructor(
public email: string,
public name: string,
public age: number
) {}
static fromEmail(email: string): User {
return new User(email, 'Unknown', 0);
}
static fromNameAndAge(name: string, age: number): User {
return new User(`${name.toLowerCase()}@example.com`, name, age);
}
static defaultUser(): User {
return new User('default@example.com', 'Guest', 18);
}
}
// Использование:
const user1 = User.fromEmail("test@test.com");
const user2 = User.fromNameAndAge("Alice", 25);
Этот подход более читаемый и масштабируемый, особенно если логика инициализации различается для каждого случая.
Важные советы
- Не злоупотребляйте перегрузкой: если вариантов больше 3-4, подумайте о рефакторинге.
- Всегда указывайте типы параметров в реализации как
anyили используйте union types, если это необходимо, но лучше — опциональные параметры с проверкой типов. - Помните, что перегрузка работает только на этапе компиляции. В скомпилированном JavaScript останется только одна функция.
Использование union types и discriminated unions
Можно пойти другим путём — принимать один параметр, который является объединением (union) нескольких типов. Это особенно удобно, когда варианты сильно отличаются по структуре:
type UserInit =
| { type: 'email'; email: string }
| { type: 'nameAge'; name: string; age: number }
| { type: 'default' };
class User {
email: string;
name: string;
age: number;
constructor(init: UserInit = { type: 'default' }) {
switch (init.type) {
case 'email':
this.email = init.email;
this.name = 'Unknown';
this.age = 0;
break;
case 'nameAge':
this.email = `${init.name.toLowerCase()}@example.com`;
this.name = init.name;
this.age = init.age;
break;
case 'default':
this.email = 'default@example.com';
this.name = 'Guest';
this.age = 18;
break;
}
}
}
// Использование:
const user1 = new User({ type: 'email', email: 'alice@mail.com' });
const user2 = new User({ type: 'nameAge', name: 'Bob', age: 30 });
const user3 = new User(); // по умолчанию
Плюсы:
- Полная типобезопасность — TypeScript будет проверять, какие поля обязательны для каждого варианта.
- Легко добавлять новые варианты без изменения сигнатуры конструктора.
- Код получается самодокументируемым.
Минусы:
- Вызов конструктора становится более многословным (нужно передавать объект с полем
type). - Не подходит, если нужно сохранить лаконичный синтаксис
new User("email").
Этот подход хорошо сочетается с паттерном "строитель" (builder) или фабриками, если количество вариантов растёт.