Разрешено ли множественное наследование в TypeScript?

Ответ

Нет, TypeScript, как и JavaScript, не поддерживает множественное наследование классов в классическом понимании (когда класс наследует реализацию от нескольких родительских классов). Попытка написать class C extends A, B {} вызовет ошибку.

Однако, для повторного использования кода из нескольких источников в TypeScript используются альтернативные подходы:

  1. Миксины (Mixins): Это функции, которые принимают класс и возвращают новый класс с расширенной функциональностью.

    class CanLog {
        log(msg: string) { console.log(msg); }
    }
    
    class CanSerialize {
        serialize() { return JSON.stringify(this); }
    }
    
    type Constructor = new (...args: any[]) => {};
    
    function Loggable<TBase extends Constructor>(Base: TBase) {
        return class extends Base {
            log = CanLog.prototype.log;
        };
    }
    
    function Serializable<TBase extends Constructor>(Base: TBase) {
        return class extends Base {
            serialize = CanSerialize.prototype.serialize;
        };
    }
    
    class MyService {}
    const EnhancedService = Serializable(Loggable(MyService));
    const instance = new EnhancedService();
    instance.log('Hello'); // OK
    instance.serialize(); // OK
  2. Композиция: Часто является более предпочтительным и гибким подходом. Вместо наследования класс содержит экземпляры других классов (делегирует им работу).

    class Logger { log(msg: string) { /* ... */ } }
    class Serializer { serialize(data: any) { /* ... */ } }
    
    class MyService {
        private logger = new Logger();
        private serializer = new Serializer();
    
        process(data: any) {
            this.logger.log('Processing');
            const result = this.serializer.serialize(data);
            // ...
        }
    }
  3. Реализация нескольких интерфейсов: Класс может реализовывать несколько интерфейсов, что гарантирует наличие определенных методов, но не предоставляет их реализацию.

    interface Loggable { log(): void; }
    interface Serializable { serialize(): string; }
    
    class MyComponent implements Loggable, Serializable {
        log() { /* реализация */ }
        serialize() { /* реализация */ }
    }

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

Ответ 18+ 🔞

Да ты посмотри, какая история интересная вырисовывается! TypeScript, как и его предок JavaScript, наотрез отказывается от классического множественного наследования, где можно тупо написать class C extends A, B {}. Ну, то есть попробовать-то можно, но получишь ошибку прямо в лицо, как будто наступил на грабли — ёпта, больно и обидно.

Но народ наш не лыком шит, и придумали обходные пути, чтобы не остаться с носом.

  1. Миксины (Mixins). Это, блядь, такие хитрожопые функции, которые берут один класс и натягивают на него функциональность другого, как презерватив. Выглядит, конечно, немного как шаманство с бубном, но работает.

    // Вот два простых класса с одной фичей каждый
    class CanLog {
        log(msg: string) { console.log(msg); }
    }
    
    class CanSerialize {
        serialize() { return JSON.stringify(this); }
    }
    
    // А это магический тип для конструктора, без него никуда
    type Constructor = new (...args: any[]) => {};
    
    // Сами миксины — функции, которые возвращают новый класс
    function Loggable<TBase extends Constructor>(Base: TBase) {
        return class extends Base {
            log = CanLog.prototype.log; // Подтягиваем метод
        };
    }
    
    function Serializable<TBase extends Constructor>(Base: TBase) {
        return class extends Base {
            serialize = CanSerialize.prototype.serialize;
        };
    }
    
    // Берём голый класс...
    class MyService {}
    // ...и накручиваем на него миксины, как тамагочи на шею
    const EnhancedService = Serializable(Loggable(MyService));
    const instance = new EnhancedService();
    instance.log('Hello'); // Опа, работает!
    instance.serialize(); // И это тоже! Удивление пиздец.
  2. Композиция. А вот это, я тебе скажу, мой любимый способ. Забудь про "является", думай "имеет". Вместо того чтобы пытаться родиться от двух отцов (что, согласись, странно), твой класс просто содержит в себе нужные штуки и пользуется ими. Гораздо проще и надёжнее, доверия к этому подходу — овердохуища.

    // Отдельные, независимые модули
    class Logger { log(msg: string) { /* ... */ } }
    class Serializer { serialize(data: any) { /* ... */ } }
    
    class MyService {
        // Не наследуем, а владеем
        private logger = new Logger();
        private serializer = new Serializer();
    
        process(data: any) {
            this.logger.log('Processing'); // Делегируем работу
            const result = this.serializer.serialize(data);
            // ...
        }
    }

    Чувак, это как собрать комп из готовых блоков, а не пытаться вырастить один супер-блок со всеми функциями сразу.

  3. Несколько интерфейсов. А это про контракты. Ты можешь классу сказать: "Слушай, дружок, ты обязан уметь log() и serialize(). Как именно — твои проблемы, но методы должны быть!" Чистая формальность, но иногда очень полезная.

    interface Loggable { log(): void; }
    interface Serializable { serialize(): string; }
    
    class MyComponent implements Loggable, Serializable {
        log() { /* реализуй сам, я не мамка твоя */ }
        serialize() { /* тут тоже */ }
    }

Если честно, в жизни я чаще всего пользуюсь композицией и интерфейсами. Код получается менее спагеттиобразным, его проще тестировать и менять. Миксины — это мощно, но выглядят они иногда как пиздопроебибна какая-то, и не каждый в команде сразу поймёт, что тут происходит. Так что выбирай по ситуации, но помни — прямое множественное наследование в TypeScript это хуй с горы, его нет.