TypeScript (как и JavaScript) поддерживает только одиночное наследование — класс может расширять лишь один базовый класс. Миксины решают эту проблему, позволяя «примешивать» поведение из нескольких источников.
// Хотим: class Bird extends Flyable, Swimmable, Walkable
// Но нельзя: class Bird extends Animal, Flyable // Ошибка TS!Ключевой тип для миксинов — Constructor:
// Описывает любой класс-конструктор
type Constructor<T = {}> = new (...args: any[]) => T
// Миксин — функция, принимающая класс и возвращающая расширенный класс
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this)
}
static deserialize(json: string) {
return JSON.parse(json)
}
}
}type Constructor<T = {}> = new (...args: any[]) => T
// Миксин 1: добавляет timestamp поля
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date()
updatedAt = new Date()
touch() {
this.updatedAt = new Date()
return this
}
}
}
// Миксин 2: добавляет soft-delete
function SoftDeletable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
deletedAt: Date | null = null
delete() {
this.deletedAt = new Date()
}
restore() {
this.deletedAt = null
}
get isDeleted() {
return this.deletedAt !== null
}
}
}
// Базовый класс
class Entity {
constructor(public id: number) {}
}
// Применяем оба миксина
class User extends Timestamped(SoftDeletable(Entity)) {
constructor(id: number, public name: string) {
super(id)
}
}
const user = new User(1, 'Алексей')
console.log(user.createdAt) // Date
user.delete()
console.log(user.isDeleted) // true
user.restore()
console.log(user.isDeleted) // falseДля корректной типизации интерфейс и миксин объявляют вместе:
interface Activatable {
isActive: boolean
activate(): void
deactivate(): void
}
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base implements Activatable {
isActive = false
activate() { this.isActive = true }
deactivate() { this.isActive = false }
toggle() { this.isActive = !this.isActive }
}
}type Constructor<T = {}> = new (...args: any[]) => T
function EventEmitter<TBase extends Constructor>(Base: TBase) {
return class extends Base {
private _handlers = new Map<string, Function[]>()
on(event: string, handler: Function) {
const list = this._handlers.get(event) ?? []
this._handlers.set(event, [...list, handler])
return this
}
off(event: string, handler: Function) {
const list = this._handlers.get(event) ?? []
this._handlers.set(event, list.filter(h => h !== handler))
}
emit(event: string, ...args: any[]) {
(this._handlers.get(event) ?? []).forEach(h => h(...args))
}
}
}
class Store extends EventEmitter(class {}) {
private state: Record<string, any> = {}
set(key: string, value: any) {
this.state[key] = value
this.emit('change', key, value)
}
}Паттерн миксинов: комбинирование поведений через функции-обёртки над классами
// TypeScript: type Constructor<T> = new (...args: any[]) => T
// function Mixin<TBase extends Constructor>(Base: TBase) { ... }
// В JS — то же самое, просто без аннотаций типов
// Миксин 1: Timestamped
function Timestamped(Base) {
return class extends Base {
constructor(...args) {
super(...args)
this.createdAt = new Date()
this.updatedAt = new Date()
}
touch() {
this.updatedAt = new Date()
return this
}
getAge() {
return Date.now() - this.createdAt.getTime()
}
}
}
// Миксин 2: SoftDeletable
function SoftDeletable(Base) {
return class extends Base {
constructor(...args) {
super(...args)
this.deletedAt = null
}
delete() {
this.deletedAt = new Date()
return this
}
restore() {
this.deletedAt = null
return this
}
get isDeleted() {
return this.deletedAt !== null
}
}
}
// Миксин 3: EventEmitter
function EventEmitter(Base) {
return class extends Base {
constructor(...args) {
super(...args)
this._handlers = new Map()
}
on(event, handler) {
if (!this._handlers.has(event)) this._handlers.set(event, [])
this._handlers.get(event).push(handler)
return this
}
off(event, handler) {
const list = this._handlers.get(event) ?? []
this._handlers.set(event, list.filter(h => h !== handler))
}
emit(event, ...args) {
;(this._handlers.get(event) ?? []).forEach(h => h(...args))
}
once(event, handler) {
const wrapper = (...args) => {
handler(...args)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
}
}
// Миксин 4: Validatable
function Validatable(Base) {
return class extends Base {
validate() {
const rules = this.constructor.validationRules ?? {}
const errors = []
for (const [field, rule] of Object.entries(rules)) {
if (!rule(this[field])) {
errors.push(`Поле "${field}" не прошло валидацию`)
}
}
return errors
}
isValid() {
return this.validate().length === 0
}
}
}
// Базовый класс
class Entity {
constructor(id) { this.id = id }
toString() { return `[${this.constructor.name} id=${this.id}]` }
}
// Комбинируем все миксины
class User extends EventEmitter(SoftDeletable(Timestamped(Entity))) {
constructor(id, name, email) {
super(id)
this.name = name
this.email = email
}
}
User.validationRules = {
name: (v) => typeof v === 'string' && v.length >= 2,
email: (v) => /S+@S+.S+/.test(v),
}
// Применяем Validatable к User
const ValidatableUser = Validatable(User)
// --- Демонстрация ---
console.log('=== Timestamps ===')
const user = new User(1, 'Алексей', 'alex@mail.ru')
console.log('createdAt:', user.createdAt instanceof Date) // true
console.log('toString:', user.toString())
console.log('\n=== SoftDelete ===')
console.log('isDeleted:', user.isDeleted) // false
user.delete()
console.log('isDeleted:', user.isDeleted) // true
user.restore()
console.log('isDeleted:', user.isDeleted) // false
console.log('\n=== EventEmitter ===')
user.on('update', (field, val) => {
console.log(`Событие update: ${field} = ${val}`)
})
user.emit('update', 'name', 'Иван')
user.emit('update', 'email', 'ivan@mail.ru')
console.log('\n=== Validatable ===')
const vu = new ValidatableUser(2, 'А', 'не-email')
console.log('Ошибки:', vu.validate()) // два поля не прошли
const vu2 = new ValidatableUser(3, 'Ольга', 'olga@mail.ru')
console.log('Ошибки:', vu2.validate()) // []
console.log('isValid:', vu2.isValid()) // trueTypeScript (как и JavaScript) поддерживает только одиночное наследование — класс может расширять лишь один базовый класс. Миксины решают эту проблему, позволяя «примешивать» поведение из нескольких источников.
// Хотим: class Bird extends Flyable, Swimmable, Walkable
// Но нельзя: class Bird extends Animal, Flyable // Ошибка TS!Ключевой тип для миксинов — Constructor:
// Описывает любой класс-конструктор
type Constructor<T = {}> = new (...args: any[]) => T
// Миксин — функция, принимающая класс и возвращающая расширенный класс
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this)
}
static deserialize(json: string) {
return JSON.parse(json)
}
}
}type Constructor<T = {}> = new (...args: any[]) => T
// Миксин 1: добавляет timestamp поля
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date()
updatedAt = new Date()
touch() {
this.updatedAt = new Date()
return this
}
}
}
// Миксин 2: добавляет soft-delete
function SoftDeletable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
deletedAt: Date | null = null
delete() {
this.deletedAt = new Date()
}
restore() {
this.deletedAt = null
}
get isDeleted() {
return this.deletedAt !== null
}
}
}
// Базовый класс
class Entity {
constructor(public id: number) {}
}
// Применяем оба миксина
class User extends Timestamped(SoftDeletable(Entity)) {
constructor(id: number, public name: string) {
super(id)
}
}
const user = new User(1, 'Алексей')
console.log(user.createdAt) // Date
user.delete()
console.log(user.isDeleted) // true
user.restore()
console.log(user.isDeleted) // falseДля корректной типизации интерфейс и миксин объявляют вместе:
interface Activatable {
isActive: boolean
activate(): void
deactivate(): void
}
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base implements Activatable {
isActive = false
activate() { this.isActive = true }
deactivate() { this.isActive = false }
toggle() { this.isActive = !this.isActive }
}
}type Constructor<T = {}> = new (...args: any[]) => T
function EventEmitter<TBase extends Constructor>(Base: TBase) {
return class extends Base {
private _handlers = new Map<string, Function[]>()
on(event: string, handler: Function) {
const list = this._handlers.get(event) ?? []
this._handlers.set(event, [...list, handler])
return this
}
off(event: string, handler: Function) {
const list = this._handlers.get(event) ?? []
this._handlers.set(event, list.filter(h => h !== handler))
}
emit(event: string, ...args: any[]) {
(this._handlers.get(event) ?? []).forEach(h => h(...args))
}
}
}
class Store extends EventEmitter(class {}) {
private state: Record<string, any> = {}
set(key: string, value: any) {
this.state[key] = value
this.emit('change', key, value)
}
}Паттерн миксинов: комбинирование поведений через функции-обёртки над классами
// TypeScript: type Constructor<T> = new (...args: any[]) => T
// function Mixin<TBase extends Constructor>(Base: TBase) { ... }
// В JS — то же самое, просто без аннотаций типов
// Миксин 1: Timestamped
function Timestamped(Base) {
return class extends Base {
constructor(...args) {
super(...args)
this.createdAt = new Date()
this.updatedAt = new Date()
}
touch() {
this.updatedAt = new Date()
return this
}
getAge() {
return Date.now() - this.createdAt.getTime()
}
}
}
// Миксин 2: SoftDeletable
function SoftDeletable(Base) {
return class extends Base {
constructor(...args) {
super(...args)
this.deletedAt = null
}
delete() {
this.deletedAt = new Date()
return this
}
restore() {
this.deletedAt = null
return this
}
get isDeleted() {
return this.deletedAt !== null
}
}
}
// Миксин 3: EventEmitter
function EventEmitter(Base) {
return class extends Base {
constructor(...args) {
super(...args)
this._handlers = new Map()
}
on(event, handler) {
if (!this._handlers.has(event)) this._handlers.set(event, [])
this._handlers.get(event).push(handler)
return this
}
off(event, handler) {
const list = this._handlers.get(event) ?? []
this._handlers.set(event, list.filter(h => h !== handler))
}
emit(event, ...args) {
;(this._handlers.get(event) ?? []).forEach(h => h(...args))
}
once(event, handler) {
const wrapper = (...args) => {
handler(...args)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
}
}
// Миксин 4: Validatable
function Validatable(Base) {
return class extends Base {
validate() {
const rules = this.constructor.validationRules ?? {}
const errors = []
for (const [field, rule] of Object.entries(rules)) {
if (!rule(this[field])) {
errors.push(`Поле "${field}" не прошло валидацию`)
}
}
return errors
}
isValid() {
return this.validate().length === 0
}
}
}
// Базовый класс
class Entity {
constructor(id) { this.id = id }
toString() { return `[${this.constructor.name} id=${this.id}]` }
}
// Комбинируем все миксины
class User extends EventEmitter(SoftDeletable(Timestamped(Entity))) {
constructor(id, name, email) {
super(id)
this.name = name
this.email = email
}
}
User.validationRules = {
name: (v) => typeof v === 'string' && v.length >= 2,
email: (v) => /S+@S+.S+/.test(v),
}
// Применяем Validatable к User
const ValidatableUser = Validatable(User)
// --- Демонстрация ---
console.log('=== Timestamps ===')
const user = new User(1, 'Алексей', 'alex@mail.ru')
console.log('createdAt:', user.createdAt instanceof Date) // true
console.log('toString:', user.toString())
console.log('\n=== SoftDelete ===')
console.log('isDeleted:', user.isDeleted) // false
user.delete()
console.log('isDeleted:', user.isDeleted) // true
user.restore()
console.log('isDeleted:', user.isDeleted) // false
console.log('\n=== EventEmitter ===')
user.on('update', (field, val) => {
console.log(`Событие update: ${field} = ${val}`)
})
user.emit('update', 'name', 'Иван')
user.emit('update', 'email', 'ivan@mail.ru')
console.log('\n=== Validatable ===')
const vu = new ValidatableUser(2, 'А', 'не-email')
console.log('Ошибки:', vu.validate()) // два поля не прошли
const vu2 = new ValidatableUser(3, 'Ольга', 'olga@mail.ru')
console.log('Ошибки:', vu2.validate()) // []
console.log('isValid:', vu2.isValid()) // trueРеализуй два миксина. `Loggable(Base)` — добавляет методы `log(message)` (выводит "[ClassName] message"), `getLogs()` (возвращает массив всех логов), `clearLogs()` (очищает логи). `Configurable(Base)` — добавляет методы `setConfig(key, value)` (устанавливает конфигурацию), `getConfig(key)` (возвращает значение конфига), `getConfig()` без аргументов — возвращает копию всего конфига. Применй оба миксина к базовому классу `Service` и продемонстрируй работу.
В Loggable: this._logs = []; log добавляет в массив и выводит; getLogs возвращает [...this._logs]. В Configurable: this._config = {}; getConfig(key) — проверяй if (key !== undefined) return this._config[key]; else return { ...this._config }.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке