ZetCode

TypeScript Mixins

last modified October 15, 2026

A class in TypeScript can extend only one other class. Mixins work around this limit: a mixin is a function that takes a class and returns a new class that extends it with extra properties or methods. Several mixins can be applied one after another, so a class can be composed from independent, reusable parts, such as timestamps, logging, or serialization. TypeScript infers the type of the resulting class, so all added members are fully typed.

Basic Mixin

A mixin is a generic function whose type parameter is constrained to constructor types.

basic_mixin.ts
type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        createdAt = new Date();
    };
}

class User {
    constructor(public name: string) {}
}

const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");

console.log(user.name);
console.log(user.createdAt instanceof Date);
console.log(user instanceof User);

The Constructor type describes anything that can be called with new. Its any[] parameter list is required by TypeScript for mixin classes, so that the returned class can forward any constructor arguments to the base class.

Timestamped returns an anonymous class that extends Base and adds a createdAt property. The new class keeps the constructor of User, so it is created with a name. The instance has both properties, and it is still an instance of User.

$ npx tsx basic_mixin.ts
Alice
true
true

Combining Mixins

Mixins are combined by nesting the calls. Each one adds its members to the result of the previous one.

multiple_mixins.ts
type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        createdAt = new Date(2026, 9, 15);
    };
}

function Loggable<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        log(message: string): void {
            console.log(`[${this.constructor.name}] ${message}`);
        }
    };
}

class User {
    constructor(public name: string) {}
}

class SmartUser extends Loggable(Timestamped(User)) {}

const user = new SmartUser("Bob");
user.log(`created ${user.createdAt.toDateString()}`);
user.log(`name is ${user.name}`);

Loggable(Timestamped(User)) produces a class with name, createdAt, and log. Extending the result with a named class, SmartUser, gives the composed class a proper name, which appears in the log messages through this.constructor.name. The fixed date makes the output reproducible.

$ npx tsx multiple_mixins.ts
[SmartUser] created Thu Oct 15 2026
[SmartUser] name is Bob

Mixins with Methods

A mixin can add methods that work with the members it adds and with this.

mixin_with_methods.ts
type Constructor<T = {}> = new (...args: any[]) => T;

function Aged<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        birthYear = 1990;

        ageIn(year: number): number {
            return year - this.birthYear;
        }

        isAdultIn(year: number): boolean {
            return this.ageIn(year) >= 18;
        }
    };
}

class User {
    constructor(public name: string) {}
}

const AgedUser = Aged(User);
const user = new AgedUser("Charlie");
user.birthYear = 2010;

console.log(`${user.name} is ${user.ageIn(2026)}`);
console.log(user.isAdultIn(2026), user.isAdultIn(2030));

The methods of the mixin call each other through this, like methods of an ordinary class. The year is passed as an argument instead of reading the current date, which keeps the methods predictable and easy to test.

$ npx tsx mixin_with_methods.ts
Charlie is 16
false true

Mixins with Properties

Mixins can add properties with default values, together with the methods that manage them.

mixin_with_properties.ts
type Constructor<T = {}> = new (...args: any[]) => T;

type Role = "user" | "editor" | "admin";

function RoleBased<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        role: Role = "user";

        promote(role: Role): this {
            this.role = role;
            return this;
        }

        can(action: "read" | "write" | "delete"): boolean {
            const allowed: Record<Role, string[]> = {
                user: ["read"],
                editor: ["read", "write"],
                admin: ["read", "write", "delete"],
            };
            return allowed[this.role].includes(action);
        }
    };
}

class User {
    constructor(public name: string) {}
}

const user = new (RoleBased(User))("Diana");
console.log(user.role, user.can("write"));

user.promote("editor");
console.log(user.role, user.can("write"), user.can("delete"));

The role property starts as "user". The promote method returns this, so its return type is the full composed type, which allows method chaining. The Role union makes sure that only valid roles can be assigned.

$ npx tsx mixin_with_properties.ts
user false
editor true false

Overriding Methods

A mixin can override a method of the base class and call the original through super.

mixin_with_overrides.ts
type Constructor<T = {}> = new (...args: any[]) => T;

function Polite<TBase extends Constructor<{ greet(): string }>>(Base: TBase) {
    return class extends Base {
        override greet(): string {
            return `${super.greet()} Nice to meet you!`;
        }
    };
}

class User {
    constructor(public name: string) {}

    greet(): string {
        return `Hi, I'm ${this.name}.`;
    }
}

const PoliteUser = Polite(User);

console.log(new User("Eve").greet());
console.log(new PoliteUser("Eve").greet());

The constraint Constructor<{ greet(): string }> tells the compiler that the base class has a greet method, so the mixin can override it and call super.greet(). The original class is not changed; only instances of PoliteUser use the extended greeting.

$ npx tsx mixin_with_overrides.ts
Hi, I'm Eve.
Hi, I'm Eve. Nice to meet you!

Constrained Mixins

A constraint also restricts which classes a mixin can be applied to.

constrained_mixin.ts
type Constructor<T = {}> = new (...args: any[]) => T;

function Activatable<TBase extends Constructor<{ id: number }>>(Base: TBase) {
    return class extends Base {
        active = false;

        activate(): string {
            this.active = true;
            return `entity ${this.id} activated`;
        }
    };
}

class Account {
    constructor(public id: number, public owner: string) {}
}

const ActivatableAccount = Activatable(Account);
const account = new ActivatableAccount(7, "Frank");

console.log(account.activate(), account.active);

The mixin needs an id property, and its constraint says so. Account has one, so the mixin can be applied.

$ npx tsx constrained_mixin.ts
entity 7 activated true
constrained_error.ts
type Constructor<T = {}> = new (...args: any[]) => T;

function Activatable<TBase extends Constructor<{ id: number }>>(Base: TBase) {
    return class extends Base {
        active = false;
    };
}

class Note {
    constructor(public text: string) {}
}

const ActivatableNote = Activatable(Note);

A Note has no id, so the compiler rejects the call. Without the constraint, the mixin could not use this.id at all.

$ npx tsc --noEmit
constrained_error.ts(13,37): error TS2345: Argument of type 'typeof Note' is not assignable to parameter of type 'Constructor<{ id: number; }>'.
  Types of construct signatures are incompatible.
    Type 'new (text: string) => Note' is not assignable to type 'new (...args: any[]) => { id: number; }'.
      Property 'id' is missing in type 'Note' but required in type '{ id: number; }'.

Typing Mixin Instances

The class returned by a mixin is anonymous. To use its instance type in annotations, it is derived with utility types.

mixin_types.ts
type Constructor<T = {}> = new (...args: any[]) => T;

function Taggable<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        tags: string[] = [];

        tag(...tags: string[]): void {
            this.tags.push(...tags);
        }
    };
}

class Article {
    constructor(public title: string) {}
}

const TaggableArticle = Taggable(Article);
type TaggableArticle = InstanceType<typeof TaggableArticle>;

function describe(article: TaggableArticle): string {
    return `${article.title} [${article.tags.join(", ")}]`;
}

const post = new TaggableArticle("Mixins in TypeScript");
post.tag("typescript", "oop");

console.log(describe(post));

typeof TaggableArticle is the type of the class, and InstanceType turns it into the type of its instances. Giving the type the same name as the constant is a common pattern; TypeScript keeps values and types in separate namespaces. The describe function can now require exactly this composed type.

$ npx tsx mixin_types.ts
Mixins in TypeScript [typescript, oop]

The applyMixins Pattern

An alternative pattern copies methods from several classes onto a target class at runtime, and uses interface merging to describe the result.

apply_mixins.ts
class Jumpable {
    jump(): string {
        return "jumping";
    }
}

class Duckable {
    duck(): string {
        return "ducking";
    }
}

class Sprite {
    constructor(public name: string) {}
}

interface Sprite extends Jumpable, Duckable {}

function applyMixins(target: Function, sources: Function[]): void {
    for (const source of sources) {
        for (const name of Object.getOwnPropertyNames(source.prototype)) {
            if (name !== "constructor") {
                Object.defineProperty(
                    target.prototype,
                    name,
                    Object.getOwnPropertyDescriptor(source.prototype, name) ?? {},
                );
            }
        }
    }
}

applyMixins(Sprite, [Jumpable, Duckable]);

const player = new Sprite("player");
console.log(player.name, player.jump(), player.duck());

The interface declaration with the same name as the class merges with it, so the compiler knows that Sprite has jump and duck. applyMixins then copies the methods to the prototype. This pattern does not check that the copying really happens and does not support properties initialized in constructors, so the class-returning mixins shown above are usually preferable.

$ npx tsx apply_mixins.ts
player jumping ducking

Best Practices

Source

TypeScript Handbook: Mixins

This tutorial covered mixins in TypeScript. They build classes from small, reusable pieces while keeping full type information.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.