8

I'm migrating a functional mixin project from javascript to typescript. All my javascript minxins have constructor signatures with a single parameter constructor(props){} .

In typescript I have defined a mixin constructor type from following the official docs at https://www.typescriptlang.org/docs/handbook/mixins.html :

export type ConstrainedMixin<T = {}> = new (...args: any[]) => T;

Even if I change that mixin signature to:

export type ConstrainedMixin<T = {}> = new (props: any) => T;

and update the implementations TSC will throw an error:

TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'.

This is unfortunate because it doesn't enable creating unique type signatures for the parameters passed to the constructor. I also now need to migrate all my existing constructors. How can I create more explicit type interfaces for the mixin constructors?

I have created a playground example

enter image description here

You can see in this screen shot that the compiler errors on the MIXIN definition and says a mixin class must have a single rest parameter. even though the type definition is:

type ConstrainedMixin<T = {}> = new (props: any) => T;

In the example I have const mg = new MixedGeneric({cool: 'bro'}); I would like to create an interface for that {cool: 'bro'} object and enforce it from within the mixin definition. I"m not sure how to properly do this. If the constructor is ...args: any[]


Update sounds like this may be some anti pattern so here is further explanation. I am building an entity component system. In my current implementation I have chains of mixins like:

const MixedEntity = RenderMixin(PhysicsMixin(GeometryMixin(Entity));
const entityInstance = new MixedEntity({bunch: "of", props: "that mixins use"});

When the final MixedEntity is instantiated it is passed a props data bag object. All the Mixins have their own initialization logic in their constructors that looks for specific properties on the props object.

where my previous mixin classes had constructors like:

constructor(props){
  super(props);
  if(props.Thing) // do props.thing
}

I now have to migrate the constructors to :

constructor(...args: any[]){
  const props = args[0]
  super(props);
  if(props.Thing) // do props thing
}   
hackape
  • 18,643
  • 2
  • 29
  • 57
kevzettler
  • 4,783
  • 15
  • 58
  • 103
  • 3
    A mixin can't be used for modifying the constructor. You use a mixin to add properties and functions, but the constructor belongs to the base class. Otherwise the "mixin" would just be a subclass and you'd be back to single inheritance. – JDB Oct 20 '20 at 19:09
  • 1
    "_You can even look at normal subclass inheritance as a degenerate form of mixin inheritance where the superclass is known at class definition time, and there's only one application of it._" - https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/ (article linked from the TypeScript docs). Building your mixin to a particular constructor signature severely and unnecessarily limits the use of your mixin. – JDB Oct 20 '20 at 19:13
  • Can you setup a typescript playground to show case what exactly is the problem? It's not clear from description. – hackape Oct 21 '20 at 04:13
  • @hackape added playground example and screenshot – kevzettler Oct 22 '20 at 16:49
  • @kevzettler I’m sorry I didn’t make my point clear enough. `A mixin class must have a constructor with a single rest parameter of type 'any[]'` that’s a known requirement/restriction of typescript. It’s not a bug. Given this condition, what exactly is your desired use case that couldn’t be implemented because of this restriction? – hackape Oct 22 '20 at 18:05
  • @hackape going back to the typescript play ground where I have implemented `const mg = new MixedGeneric({cool: 'bro'});` How would I add type definitions for that constructor argument? because it enforces the rest parameter. – kevzettler Oct 22 '20 at 20:29
  • Answered. I didn’t realize what your problem really is. Looking back and find @JDB has already pointed it out in the comment. Kudos to him. – hackape Oct 23 '20 at 03:42

2 Answers2

17

Mixin pattern in TS is meant to extend the base class with extra methods or properties, but not to tamper the constructor signature. So the derived class is supposed to keep its constructor signature identical to the base class it extends.

This the reasoning behind this A mixin class must have a constructor with a single rest parameter of type 'any[]' restriction cus TS doesn’t care, it’ll just pass the construction down to super(…args) and let it do the job.

So if you want to constrain constructor params, you just do it in the base class constructor signature.


type Constructor = new (...args: any[]) => {};
function MixinGeneric<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    // …mixin traits
  }
}

class Generic {
  // ctor param constraint goes here:
  constructor<P extends { cool: string }>(props: P) {}
}

// and it’ll check:
const mg = new MixedGeneric({cool: 'bro'});

Update in response to OP's update

Yes tampering constructor signature is considered an anti-pattern. But coders break rules all the time. As long as you understand what you're doing, do be creative.

Check it out in the Playground.

The trick is to bypass TS restriction by using type assertion and bunch of utility types.

// Utility types:

type GetProps<TBase> = TBase extends new (props: infer P) => any ? P : never
type GetInstance<TBase> = TBase extends new (...args: any[]) => infer I ? I : never
type MergeCtor<A, B> = new (props: GetProps<A> & GetProps<B>) => GetInstance<A> & GetInstance<B>


// Usage:
// bypass the restriction and manually type the signature
function GeometryMixin<TBase extends MixinBase>(Base: TBase) {
  // key 1: assert Base as any to mute the TS error
  const Derived = class Geometry extends (Base as any) {
    shape: 'rectangle' | 'triangle'
    constructor(props: { shape: 'rectangle' | 'triangle' }) {
      super(props)
      this.shape = props.shape
    }
  }

  // key 2: manually cast type to be MergeCtor
  return Derived as MergeCtor<typeof Derived, TBase>
}
hackape
  • 18,643
  • 2
  • 29
  • 57
  • I'm concerned with the constructor of the `MixinGeneric` which you've omitted here not the base class `Generic` its is in the play ground example. The `Mixin` class has its own setup logic in the constructor that it expects a interface on the props – kevzettler Oct 23 '20 at 16:08
  • sorry I mean on the `Base` class – kevzettler Oct 23 '20 at 16:17
  • Can you post some more concrete example to illustrate what problem are you facing? Since that restriction of typescript is definitely to stay, there’s no point discussing it any further. So please share more details about what problem are you facing and maybe we can think about some work around. – hackape Oct 23 '20 at 16:36
  • 1
    I’ve wondered about this myself and in my case the mixin wasn’t trying to “tamper the constructor signature”, as you put it, but rather to call it to create a new instance with modified arguments from within a method. Based on the way mixins are implemented, a mixin is not able to restrict the constructor signature of the base such that it can call new Base(). The workaround is that it can require that the mixin contains a method which returns a new instance, but if that factory method is static that’s a whole other mess. – Linda Paiste Oct 24 '20 at 18:49
  • @hackape have added a further example and clarification on the intended entity component system use case. – kevzettler Oct 26 '20 at 21:59
  • @hackape your example code looks good but the playground links to the initial playground I posted? – kevzettler Oct 27 '20 at 15:41
  • 1
    @kevzettler ah, must be some copy paste mistake. Fixed. Thanks for pointing out. – hackape Oct 27 '20 at 16:45
3

Other answers have already explained the why and I agree with the sentiment that what you're attempting seems like an anti-pattern, but in any case I'd prefer your project be migrated to TypeScript sooner than later (but eventually refactored), so I'll show how to get around the type error. Playground Link

The reason this works is because only the type signature of the implementation is checked for the mixin error, but not any of the overload declarations. In this case we only have one overload declaration. The implementation signature is not used as an overload variant. It's only used to type-check the body of the function and it must be compatible with all of the overloads (i.e. more general).

lazytype
  • 927
  • 6
  • 11
  • your play ground has 2 lines of `export default function MIXIN` is this a mistake? – kevzettler Oct 26 '20 at 21:46
  • I have already started moving my code to the `constructor(...args: any[]) { const [props] = args; super(props) console.log('lol'); }` pattern. You're saying that is bad? – kevzettler Oct 26 '20 at 21:47
  • 1
    `> your play ground has 2 lines of export default function MIXIN is this a mistake?` Nope, it's intentional. Note that the two lines each have a different type signature. This is what I meant by overload declarations. Refer to https://www.typescriptlang.org/docs/handbook/functions.html#overloads . – lazytype Oct 27 '20 at 02:31
  • 1
    _> You're saying that is bad?_ I really meant more generally that mixins are an antipattern and composition is widely considered a better approach. – lazytype Oct 27 '20 at 02:41
  • 1
    I agree with the prefer composition sentiment. do you have any resources for patterns like that in TS? looking for composition methods led me to this mixin class pattern in the first place. In fact this question clearly says to use mixins: https://stackoverflow.com/questions/48757095/typescript-class-composition and suggests the patterns we have discussed here – kevzettler Oct 27 '20 at 15:35
  • 1
    What I imagine that'd look like for your use-case is instead of having mixins like, GeometryMixin, PhysicsMixin, and RenderMixin, you'd have subclasses of Entity for each one. A classic object-oriented approach might look something like this: https://tsplay.dev/qWJrZW – lazytype Oct 27 '20 at 16:34
  • 2
    Thought I'd also share an alternative workaround to the original one I posted. It may or may not be more digestible to some readers https://tsplay.dev/rw2QxW – lazytype Oct 27 '20 at 16:43