Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 tasks done
trusktr opened this issue Jun 20, 2019 · 4 comments
Closed
5 tasks done

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

trusktr opened this issue Jun 20, 2019 · 4 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@trusktr
Copy link
Contributor

trusktr commented Jun 20, 2019

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.
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jun 26, 2019
@trusktr
Copy link
Contributor Author

trusktr commented Jul 23, 2019

@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)

@trusktr
Copy link
Contributor Author

trusktr commented Jul 23, 2019

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

@trusktr
Copy link
Contributor Author

trusktr commented Jul 23, 2019

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?

@trusktr
Copy link
Contributor Author

trusktr commented Dec 28, 2019

Closing as duplicate of #32080

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants