Skip to content

A better way to use mixins ? #3338

Open
@denis-migdal

Description

@denis-migdal

The document gives some examples on how to use mixin here: https://www.typescriptlang.org/docs/handbook/mixins.html

However the 2 proposed solutions has issues:

  • the first one prevent us from having private/protected members, and may generate quite a lot of type deduction error in the function.
  • the second is not that great regarding to type safety.

I think we have a better solution to use mixin, that could be added to the documentation:

class Base {}

// declare the added properties outside of the mixin function
abstract class Mixin {}

class K extends mix(Base, Mixin) { }

Such generic function could be implemented like this:

const MixSrc = Symbol();

type Mix<
            Base  extends          new(...args:any[])=>any,
            Mixin extends abstract new(...args:any[])=>any,
        > = Omit<Base & Mixin, "new"> & (new(...args:ConstructorParameters<Base>) => (InstanceType<Base> & InstanceType<Mixin>))

function mix<
                Base  extends          new(...args:any[]) => any,
                Mixin extends abstract new(...args:any[]) => any
            >(base: Base, mixin: Mixin): Mix<Base, Mixin> {

    class _ extends base {}

    const static_props = Object.getOwnPropertyDescriptors(mixin);
    delete static_props.prototype;
    delete static_props.name;
    Object.defineProperties( _, static_props );

    ((_ as any)[MixSrc] ??= []).push(mixin);

    const hasInstance = mixin[Symbol.hasInstance];
    Object.defineProperty(mixin, Symbol.hasInstance, {
        value: function (instance: any) {
            if( instance.constructor[MixSrc].includes(this) )
                return true;
            return hasInstance.call(this, instance);
        },
        writable: false,
    });

    const instance_props = Object.getOwnPropertyDescriptors(mixin.prototype);
    // @ts-ignore
    delete instance_props.constructor;
    Object.defineProperties( _.prototype, instance_props );

    return _ as any; // well...
}

Playground Link

There are 2 limitations of this method:

  • This may not work if Mixin inherit from another class ( though you could just call mix for each of the Mixin bases ).
  • This will not work if Mixin has private # properties.

If we have private properties, we can do something like:

abstract class Mixin {
     // declare the public/protected interface here
}

type ExcludeProtected<T> = {[K in keyof T]: T[K]}
function addMixin<...>(base): Mix<Base, Mixin> {
      return class _ extends base implements ExcludeProtected<Mixin> {
             // implements Mixin methods here...
             // can't guarantee the protected interface (due to TS limitations).
      } as any;
}

But then Mixin must not inherit from another class.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions