Skip to content

Feature request: Support for using mixins inside other mixins. #32004

Closed

Description

@trusktr

Search Terms

Suggestion

Based on https://stackoverflow.com/questions/56680049, it seems mixin classes can't be used inside other mixins.

It'd be great if, in that example, the outer mixin would be able to successfully use the props and types from the Sizeable mixin.

Note, in the example, Sizeable is a class with a default Base class applied, and .mixin is the mixin function used to create it.

Basically, the following (in JS, and simplified):

function SizeableMixin(Base = class {}) {
  return class Sizeable extends Base {...}
}

const Sizeable = SizeableMixin()
Sizeable.mixin = SizeableMixin

export default Sizeable

So it's just a regular class-factory mixin (with a default application so that it is easy to extend like a normal class when used as a base class).

Use Cases

To be able to use Mixins anywhere, including inside other mixins.

Examples

import Sizeable from './Sizeable'

function FooMixin<T extends Constructor>(Base: T) {
  const Parent = Sizeable.mixin(Base)
  return class Foo extends Parent { ... } // ERROR, [Parent] is not a constructor function.
}

const Foo = FooMixin()
Foo.mixin = FooMixin // ignore types here, for now

export default Foo

But actually, Parent is a constructor. We know it is. It'd be great to use it.

In the Foo mixin, I should at least be able to correctly use Sizeable properties and methods, as if Sizeable were the base class.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Activity

  1. trusktr commented on Jul 23, 2019

    @trusktr
    ContributorAuthor

    Ryan Cavanaugh (@RyanCavanaugh) I've been working away the problems using Constructor helpers with explicit as unknown as Constructor<Instance> & Static casts, using Constructor.

    Here's an example that shows the use of a custom Constructor helper to work around the problems:

    type Constructor<T = object, A extends any[] = any[], Static = {}> = (new (
      ...a: A
    ) => T) &
      Static;
    
    function Constructor<T = object, Static = {}>(
      Ctor: Constructor<any>
    ): Constructor<T> & Static {
      return Ctor as any; // we're confident about this any cast.
    }
    
    type MixinResult<
      TClass extends Constructor,
      TBase extends Constructor
    > = Constructor<InstanceType<TClass> & InstanceType<TBase>> & TClass & TBase;
    
    class BaseClass {
      foo!: string;
      static bar = 123;
    }
    
    const base = new BaseClass();
    base.bar; // correctly not available here
    
    class Test extends BaseClass {
      static bat = "bob";
      notStatic = 4;
    }
    
    function factory<T extends Constructor<BaseClass>>(cls: T) {
      class FactoryResult extends Constructor<BaseClass, typeof BaseClass>(cls) {
        bar!: string;
        make(): InstanceType<typeof BaseClass> {
          return this;
        }
      }
    
      return FactoryResult as MixinResult<typeof FactoryResult, T>;
    }
    
    const factoryResult = factory(Test);
    factoryResult.bat; // good, available here
    
    factoryResult.foo; // good, foo is not a static property
    factoryResult.bar = 42; // good, bar is static
    factoryResult.bar = "blah"; // good, static bar is not a string
    
    const foo = new factoryResult();
    foo.notStatic; // good, available here
    
    foo.foo; // good, correctly available
    foo.bar = "blah"; // good, instance property bar is defined on line 24.
    foo.bar = 42; // good, instance prop bar is not a number
    
    function factoryG<C extends typeof BaseClass>(cls: C) {
      return class extends Constructor<BaseClass>(cls) {
        foo = 123; // good, we get an error
        bar!: string;
      };
    }

    playground

    The example shows:

    • The class inside the mixin can not properly extend the base class, and it seems we need a type cast to make it work (hence the Constructor helper for that).
    • The return type needs to capture the type of the constructor that was passed in (the T in factory generic params)
  2. trusktr commented on Jul 23, 2019

    @trusktr
    ContributorAuthor

    I updated the above example, to show the full pattern I use (needed?) to have proper mixin classes.

  3. trusktr commented on Jul 23, 2019

    @trusktr
    ContributorAuthor

    Oh, right, so back to the main point in the OP: The pattern I just illustrated now makes it possible to compose mixins out of other mixins in an understandable way through use of the Constructor helper:

    function AnotherMixin<C extends Constructor<BaseClass>>(Base: C) {
      // compose mixins together
      class Another extends factoryG(factory(Constructor<BaseClass>(Base))) {
        feeling = 'nice'
      }
    
      return Another as MixinResult<typeof Another, C>;
    }
    
    class A extends AnotherMixin(Test) {
      oh = 'yeah'
    }

    playground

    So, to point out the main issue of the OP again, one would think it would be sufficient to write:

      return class Another extends factoryG(factory(Base)) {

    but that results in the error

    Type 'MixinResult<typeof FactoryGResult, MixinResult<typeof FactoryResult, C>>' is not a constructor function type.
    

    playground

    which makes all code that uses AnotherMixin to break.

    We know Base is a Constructor, and we're telling TypeScript that it is, by forcefully casting it.

    Would it be possible for TypeScript to just know?

  4. trusktr commented on Dec 28, 2019

    @trusktr
    ContributorAuthor

    Closing as duplicate of #32080

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions