Навесить переиспользуемое поведение на класс без множественного наследования
У класса в TypeScript ровно один базовый класс, но одинаковое поведение с временными метками нужно нескольким несвязанным классам. Реализуйте Timestamped как миксин.
Требования: Timestamped(Base) возвращает класс, сохраняющий все члены и параметры конструктора Base, добавляет createdAt: Date, выставляемое при создании, и метод touch(): void, а также сочетается с другими миксинами, например class Doc extends Timestamped(Serializable(Entity)) {}. Собственные члены полученного класса не должны иметь тип any.
type Ctor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Ctor>(Base: TBase) {
// ваш код здесь
}
Допишите реализацию.
Миксин — это функция, принимающая тип конструктора и возвращающая выражение класса, наследующее его: function Stamped<T extends Ctor>(B: T) { return class extends B { at = new Date() }; }. Ограничение конструктором пробрасывает аргументы базового класса, а раз результат сам класс, вызовы миксинов вкладываются.
- ✗Копировать методы в прототип и ждать, что тип их получит
- ✗Ограничивать базу обычным объектным типом вместо типа конструктора
- ✗Терять аргументы конструктора базового класса вместо проброса
...args
- →Как потребовать, чтобы у базы уже был член, например
id: string, до применения миксина? - →Почему явный тип возврата у миксина обычно только ухудшает композицию?
Решение
type Ctor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Ctor>(Base: TBase) {
return class Timestamped extends Base {
createdAt: Date = new Date();
updatedAt: Date | null = null;
touch(): void {
this.updatedAt = new Date();
}
};
}
Три части, каждая обязательна:
TBase extends Ctor— параметр типа ограничен типом конструктора, а не объектным типом. Только такclass ... extends Baseвообще допустимо: наследоваться можно лишь от выражения с сигнатурой конструирования....args: any[]вCtor— именно это позволяет производному классу принять параметры конструктора базы без их перечисления. Собственные члены (createdAt,touch) при этом типизированы точно.- Возвращается выражение класса, а не объект. Поэтому результат снова годится в
extends— миксины вкладываются.
class Entity {
constructor(public id: string) {}
}
class Doc extends Timestamped(Entity) {}
const d = new Doc("d-1"); // аргументы конструктора Entity пробросились
d.id; // string — член базы сохранён
d.createdAt; // Date — член миксина
d.touch();
⚠️ Не задавайте миксину явный тип возврата: он выводится как анонимный класс, и именно вывод склеивает члены базы с членами миксина. Аннотация вроде : Ctor<Timestamped> стирает члены Base.
⚠️ Object.assign(Doc.prototype, {...}) навесит методы в рантайме, но тип класса о них не узнает — это ровно та ошибка, ради которой миксин и делают функцией над классом.