Typescript: Mixin classes don't allow constructors of _generic_ 'object' types

Created on 9 Jun 2017  路  3Comments  路  Source: microsoft/TypeScript

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 https://github.com/Microsoft/TypeScript/issues/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();
  };
};
Awaiting More Feedback Suggestion

Most helpful comment

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

All 3 comments

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.

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

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()
Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbondc picture jbondc  路  3Comments

uber5001 picture uber5001  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

MartynasZilinskas picture MartynasZilinskas  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments