0

I am working on converting an existing codebase to TypeScript.

While I recognize the value of TypeScript's Mixin infrastructure, I need to support a codebase that simply copies properties from a common module to a class instance.

Here is a very simplified example to illustrate the structure I'm trying to support:

// Common base class
class BaseClass {
    baseFeature():boolean {
        return true;
    }
}

// Common mixin module
const mixin = {
    mixinFeatureOne():boolean {
        return this.baseFeature();
    },
    mixinFeatureTwo():boolean {
        return this.mixinFeatureOne();
    }
} as ThisType<BaseClass>;

// Specific module that extends the base class and combines the mixin
class DerivedClass extends BaseClass {
    constructor() {
        super();
        Object.assign(this, {
            ...mixin,
        });
    }
}

const instance = new DerivedClass();

TypeScript is choking on the mixin object.

Without a lot of further modification, I seem to be limited to two options:

  1. EITHER Leave the mixin object implicitly typed (that is, the default inferred type) in which case I can't access functions of the base type

  2. OR I can define the mixin object as ThisType<BaseClass> which gives me access to the base class functions, but I can no longer access functions within the mixin.

I'm sure I'm doing something wrong here, but the only solution I've found so far is to create a type that duplicates the mixin object then declares the mixin as a combination of the ThisType and the mixin type. This is very tedious as it requires duplicating the definition every time a function is added or modified.

type MixinType = BaseClass & {
    mixinFeatureOne():boolean;
    mixinFeatureTwo():boolean;
};

const mixin = {
    mixinFeatureOne():boolean {
        return this.baseFeature();
    },
    mixinFeatureTwo():boolean {
        return this.mixinFeatureOne();
    }
} as ThisType<MixinType>;

Maybe this is just the penalty that must be paid for such a non-conforming mixin strategy, but I feel like this should be a bit easier. Is there a way we could simplify this by extending the inferred type instead of redeclaring it? Something like this:

const mixin = {
    mixinFeatureOne():boolean {
        return this.baseFeature();
    },
    mixinFeatureTwo():boolean {
        return this.mixinFeatureOne();
    }
} as ThisType<BaseClass & __inferred_type__>;

(I am also aware that I'm going to run into issues with the DerivedType not having the mixin features. I'm happy to leave that for a future question (if I can't figure it out on my own), but I'm grateful if, in addition to the main question, you have any advice or recommendations in that regard.)

JDB
  • 25,172
  • 5
  • 72
  • 123

1 Answers1

0

YOu can explicitly type this in each mixin method:

type Mixin = {
  mixinFeatureOne: () => boolean
  baseFeature: () => boolean
}

// Common base class
class BaseClass {
  baseFeature(): boolean {
    return true;
  }
}

// Common mixin module
const mixin = {
  mixinFeatureOne(this: Mixin): boolean {
    return this.baseFeature();
  },
  mixinFeatureTwo(this: Mixin): boolean {
    return this.mixinFeatureOne();
  }
};

// Specific module that extends the base class and combines the mixin
class DerivedClass extends BaseClass {
  constructor() {
    super();
    Object.assign(this, {
      ...mixin,
    });
  }
}

const instance = new DerivedClass();
instance.

Playground

However, instance still does not know about mixin methods.

If you want more or less safe typings for Mixin pattern, consider this example:

// credits goes to https://stackoverflow.com/a/50375286
  type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends (
      k: infer I
    ) => void
    ? I
    : never;

  type ClassType = new (...args: any[]) => any;
  
  function Mixin<T extends ClassType, R extends T[]>(...classRefs: [...R]):
    new (...args: any[]) => UnionToIntersection<InstanceType<[...R][number]>> {
    return merge(class { }, ...classRefs);
  }

  function merge(derived: ClassType, ...classRefs: ClassType[]) {
    classRefs.forEach(classRef => {
      Object.getOwnPropertyNames(classRef.prototype).forEach(name => {
        // you can get rid of type casting in this way
        const descriptor = Object.getOwnPropertyDescriptor(classRef.prototype, name)
        if (name !== 'constructor' && descriptor) {
          Object.defineProperty(
            derived.prototype,
            name,
            descriptor
          );
        }
      });
    });

    return derived;
  }

  class Foo {
    foo() { }
  }

  class Bar {
    bar() { }
  }

  class Baz {
    baz() {
      console.log('baz');
    }
  }

  class MyClass extends Mixin(Foo, Bar, Baz) { }

  const my = new MyClass();
  my.foo() // ok
  my.bar() // ok
  my.baz(); // ok

Playground

Full explanation will find in my article

If you are ok with this approach, I will provide more explanation in the answer