Skip to content
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

Mixin classes don't allow constructors of _generic_ 'object' types #16390

Open
athasach opened this issue Jun 9, 2017 · 5 comments
Open

Mixin classes don't allow constructors of _generic_ 'object' types #16390

athasach opened this issue Jun 9, 2017 · 5 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

@athasach
Copy link

athasach commented Jun 9, 2017

TypeScript Version: 2.3.4, 2.4.0-dev.20170609

Code:

type Constructor<T> = new (...args: any[]) => T;

const Timestamped = <I extends object, CT extends Constructor<I>>(Base: CT) => {
  return class extends Base {
    timestamp = new Date();
  };
};

Expected behavior:

Should be able to use the generic I as an argument to Constructor.

Actual behavior:

Compiling this yields the following error:

error TS2509: Base constructor return type 'I' is not a class or interface type.

Note:

This is a variation on #13805 where the following compiles successfully:

type Constructor<T> = new (...args: any[]) => T;

const Timestamped = <CT extends Constructor<object>>(Base: CT) => {
  return class extends Base {
    timestamp = new Date();
  };
};
@athasach athasach changed the title Mixin classes don't allow constructors of generic 'object' types Mixin classes don't allow constructors of _generic_ 'object' types Jun 9, 2017
@zamb3zi
Copy link

zamb3zi commented Aug 23, 2017

I've also run into this. This is my workaround which allows a constraint on the base class (IBase) and provides the base type T to the mixin definition.

type Constructor<T> = new (...args: any[]) => T;

interface IBase {
    foo: number;
}

const Timestamped = <C extends Constructor<IBase>>(Base: C) =>
    <T extends IBase>() =>
        class extends Base {
            timestamp = new Date;
        };

class Base implements IBase {
    foo = 5;
}

class Sub extends Timestamped(Base)<Base>() {
    bar = 'ok';
}

let s = new Sub;
console.log(s.foo, s.bar, s.timestamp);

I've haven't been able to find a way to avoid the double function call.

@mhegazy mhegazy added the Needs Investigation This issue needs a team member to investigate its status. label Aug 29, 2017
@evg656e
Copy link

evg656e commented Oct 28, 2017

I wonder if it possible to return generic class from mixin, like this:

type Constructor<T> = new (...args: any[]) => T;

const List = <T extends Constructor<{}>>(Base: T) => class <T> extends Base {
    values: T[];

    constructor(...args: any[]) {
        super(...args);
    }

    add(value: T) {
        this.values.push(value);
    }
};

class Item {
    render() { }
}

class ItemList extends List(Item)<Item> {
    render() {
        this.values.forEach((item) => {
            item.render();
        });
    }
}

const list = new ItemList();
console.log(list.values);
list.render();

For now it's produces error: error TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'., but all vscode hints are ok (list.values are type of Item, its have prototype of Item and so on).

@lupuscaoticus
Copy link

lets fix the problem "is not a class or interface type"
i use TypeScript version 2.9.2/3.0.1 but think it sould work with older ones to

Basic requirement
define in your projets global.d.ts
    interface ClassType<InstanceType extends {} = {}> extends Function {
      new(...args: any[]): InstanceType
      prototype: InstanceType
    }
defined in lib.es5.d.ts
    type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;
@athasach

TypeScript infers all types correctly if you change the resolution direction

    const Timestamped = <BaseClass extends ClassType<{}>, BaseInstance extends InstanceType<BaseClass>>(base: BaseClass) =>
      class extends base {
        someTimeAgoIWasA!: BaseInstance
        constructedFrom!: BaseClass
        timestamp = new Date();
      }

To limit the accepted classe/instance type you can change the {} as in this examples:

  • BaseClass extends ClassType<Object>
  • BaseClass extends ClassType<Parent>
  • BaseClass extends ClassType<...>
example only allow to mixin to child/sub classes of the class Parent
    class Parent {
      talkToLuke() {
        console.log('I am your father!')
      }
    }

    const Timestamped = <BaseClass extends ClassType<Parent>, BaseInstance extends InstanceType<BaseClass>>(base: BaseClass) =>
      class extends base {
        someTimeAgoIWasA!: BaseInstance
        constructedFrom!: BaseClass
        timestamp = new Date();
      }
@zamb3zi

Just as above the InstanceType can be infered as folloing

example Timestamped
    const Timestamped = <BaseClass extends ClassType<{}>, BaseInstance extends InstanceType<BaseClass>>(base: BaseClass) =>
      class extends base {
        someTimeAgoIWasA!: BaseInstance
        constructedFrom!: BaseClass
        timestamp = new Date();
      }

    class Base {
      foo: number = 5
    }
    class Sub extends Timestamped(Base) {
      bar = 'ok'
    }
    const s = new Sub()
    console.log(s.foo, s.bar, s.timestamp)
@evg656e

Just as above the InstanceType can be infered as folloing

example ItemList
    const List = <BaseClass extends ClassType<{}>, BaseInstance extends InstanceType<BaseClass>>(base: BaseClass) =>
      class extends base {
        values!: BaseInstance[]
        add(value: BaseInstance) {
          this.values.push(value)
        }
      }

    class Item {
      render() { }
    }
    class ItemList extends List(Item) {
      render() {
        this.values.forEach((item) => item.render())
      }
    }
    const list = new ItemList()
    console.log(list.values)
    list.render()

@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 and removed Needs Investigation This issue needs a team member to investigate its status. labels Sep 16, 2019
@ForkKILLET
Copy link

@lupuscaoticus Hi, I'm trying to give the type of a mixin, like this

type Mixin = <S extends ClassType, I extends InstanceType<S>, M extends I> (B: S) => ClassType<M>

However tsc says Timestamped can't be a Mixin, how can I fix it?
Thank you very much!

@boconnell
Copy link

FWIW this looks like a duplicate of #13807

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

8 participants