Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why do typescript mixins require a constructor with a single rest parameter any[]?

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
}   
like image 528
kevzettler Avatar asked Jan 24 '23 16:01

kevzettler


1 Answers

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>
}
like image 185
hackape Avatar answered May 10 '23 20:05

hackape