javascripttypescriptclassmixinsclass-expression

Why I can't use Mixins with class expressions in TypeScript


I was learning about Mixins in JavaScript and decided to implement them in TypeScript. Everything works fine when I declare classes normally, but when I assign them to variables, I encounter unexpected behavior. The Mixin methods become inaccessible on objects instantiated from a class combined with a Mixin.

I want to investigate the root cause of this issue and understand why Mixin methods become inaccessible in this scenario.

// Mixin declaration
interface SpeechMixin {
    greetings(): string;
}
const speechMixin: SpeechMixin = {
    greetings() {
        return `Hello, my name is ${this.name}`;
    },
};

// Class expression
const User = class {
    constructor(public name: string) {}
};

Object.assign(User.prototype, speechMixin);

interface User extends SpeechMixin {}

const george = new User('George');
console.log(george.greetings()); // ERROR: Property 'greetings' does not exist on type 'User'.

Solution

  • While you cannot merge an interface with a class variable with the same name (thus no greetings) you could create a function that would mix objects into a prototype and assert that:

    Playground (oops, swap src and dst please)

    function mixin<M extends object>(src: new (...args: any) => any, dst: M): asserts src is new (...args: any) => unknown & M {
        Object.assign(src.prototype, dst);
    }
    
    mixin(User, speechMixin);
    

    IMPORTANT

    Actually your pattern could be NOT a mixin since you modify class prototypes, that would be considered as usual inheritance. A mixin is something that you construct from independent objects on the fly:

    Playground

    const speechMixin = class {
        declare name: string;
        greetings() {
            return `Hello, my name is ${this.name}`;
        }
    };
    
    const User = class {
        constructor(public name: string) {}
    };
    
    function mixin<C extends object, M extends object>(dst: C, src: M): asserts dst is C & M {
        Object.assign(src, dst);
    }
    
    const george = new User('George');
    mixin(george, new speechMixin);
    console.log(george.greetings());