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

Allow class to extend from a generic type parameter #4890

Closed
wycats opened this Issue Sep 20, 2015 · 32 comments

Comments

Projects
None yet
@wycats

wycats commented Sep 20, 2015

There appears to be some kind of internal "class" type that is impossible to represent generically:

You can see a live version here: http://goo.gl/exBzY6

function IdentifiableSubclass<T extends What?>(SuperClass: T) {
  return class extends T {
    public _id = null;
  }
} 

class Thing {
  public hello = null;
  /* impl */
}

const IdentifiableThing = IdentifiableSubclass(Thing);

class ChildThing extends IdentifiableThing {

}

let child = new ChildThing();
child.hello; // TS does not understand that this exists
child._id;   // TS understands that this exists
@DanielRosenwasser

This comment has been minimized.

Show comment
Hide comment
@DanielRosenwasser

DanielRosenwasser Sep 21, 2015

Member

Hey @wycats, I assume you meant return class extends SuperClass { /* ... */ }.

Yeah, I tried something like:

interface Constructable<T> {
    new (...args): T;
    prototype: T
}

function IdentifiableSubclass<T>(SuperClass: Constructable<T>) {
  return class extends SuperClass {
    public _id = null;
  }
} 

with no luck since, like you mentioned, the constructed type needs to be resolved to a class or an interface. I think the limitation should be from extending primitive types, not from non-class/interface types. @ahejlsberg can you weigh in on this?

Member

DanielRosenwasser commented Sep 21, 2015

Hey @wycats, I assume you meant return class extends SuperClass { /* ... */ }.

Yeah, I tried something like:

interface Constructable<T> {
    new (...args): T;
    prototype: T
}

function IdentifiableSubclass<T>(SuperClass: Constructable<T>) {
  return class extends SuperClass {
    public _id = null;
  }
} 

with no luck since, like you mentioned, the constructed type needs to be resolved to a class or an interface. I think the limitation should be from extending primitive types, not from non-class/interface types. @ahejlsberg can you weigh in on this?

@DanielRosenwasser

This comment has been minimized.

Show comment
Hide comment
@DanielRosenwasser

DanielRosenwasser Sep 21, 2015

Member

Actually, this comes back to not being able to know if a type parameter is a primitive or not. I think this comes back to #1809.

Member

DanielRosenwasser commented Sep 21, 2015

Actually, this comes back to not being able to know if a type parameter is a primitive or not. I think this comes back to #1809.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Sep 21, 2015

Member

@wycats Here's something that works:

interface Constructor<T> {
    new (...args): T;
}

interface Base {
}

interface Identifiable {
    _id: any;
}

function IdentifiableSubclass<T extends Base>(SuperClass: Constructor<T>) {
    class C extends (<Constructor<Base>>SuperClass) {
        public _id = null;
    }
    return <Constructor<Identifiable & T>>C;
}

class Thing {
    public hello = null;
    /* impl */
}

const ChildThing = IdentifiableSubclass(Thing);
let child = new ChildThing();

child.hello;  // Ok
child._id;    // Ok

Some comments on what's going on...

First, we have a hard constraint that you can only inherit from a class or interface type, and not from type parameter (similar to Java, C#, and pretty much any mainstream OOP language). A key reason is that we can't properly check whether the base class has properties that conflict with the derived class when we don't yet know the final shape of the base class. This also highlights the primary differences between extends and the & operator: The & operator allows the type operands to be type parameters but doesn't cause errors when properties have the same name. Instead, & recursively applies & to the types of the similarly named properties.

So, in the IdentifiableSubclass method, I'm "casting away" the type parameter (such that C effectively extends the statically known Base) and then "casting back" to the intersection type Identifiable & T. This means that child ends up having the type Identifiable & Thing.

The fact that you can only inherit from a class or interface type also (unfortunately) implies that you can't inherit from an intersection type. Therefore, you can't do the last bit of your example where you inherit ChildThing from IdentifiableThing.

I'm definitely open to suggestions on making things more flexible here, but we're pushing close to the limits of our type system (and, really, any other type system I know of).

Member

ahejlsberg commented Sep 21, 2015

@wycats Here's something that works:

interface Constructor<T> {
    new (...args): T;
}

interface Base {
}

interface Identifiable {
    _id: any;
}

function IdentifiableSubclass<T extends Base>(SuperClass: Constructor<T>) {
    class C extends (<Constructor<Base>>SuperClass) {
        public _id = null;
    }
    return <Constructor<Identifiable & T>>C;
}

class Thing {
    public hello = null;
    /* impl */
}

const ChildThing = IdentifiableSubclass(Thing);
let child = new ChildThing();

child.hello;  // Ok
child._id;    // Ok

Some comments on what's going on...

First, we have a hard constraint that you can only inherit from a class or interface type, and not from type parameter (similar to Java, C#, and pretty much any mainstream OOP language). A key reason is that we can't properly check whether the base class has properties that conflict with the derived class when we don't yet know the final shape of the base class. This also highlights the primary differences between extends and the & operator: The & operator allows the type operands to be type parameters but doesn't cause errors when properties have the same name. Instead, & recursively applies & to the types of the similarly named properties.

So, in the IdentifiableSubclass method, I'm "casting away" the type parameter (such that C effectively extends the statically known Base) and then "casting back" to the intersection type Identifiable & T. This means that child ends up having the type Identifiable & Thing.

The fact that you can only inherit from a class or interface type also (unfortunately) implies that you can't inherit from an intersection type. Therefore, you can't do the last bit of your example where you inherit ChildThing from IdentifiableThing.

I'm definitely open to suggestions on making things more flexible here, but we're pushing close to the limits of our type system (and, really, any other type system I know of).

@tejacques

This comment has been minimized.

Show comment
Hide comment
@tejacques

tejacques Dec 22, 2015

Is there an inherent reason why classes can't inherit from an intersection type? It seems like the entire shape is known in that scenario, so maybe support could be added? It would have to reconcile that intersection types could have different type declarations on the same properties, but on the surface still seems like it could be done (and disallow those with an error).

Another possibility that affords a much friendlier looking implementation would be to lift the typing up to the call site where the shape is known, and check the constraints there (much like templating in C++ as much as that thought probably horrifies everyone). At the declaration function you'd only be able to check the base type, but you could get an error at the callsite when you were doing something improper:

interface Base<T> {
  new(): T
}

function IdentifiableSubclass<T>(SuperClass: Base<T>) {
    // Only compatibility with Base<T> can be checked here
    return class C extends SuperClass {
      constructor(id) {
        super()
      }
      public _id: number = 0;
    };
}

class Thing {
    public hello = null;
    /* impl */
}

const ChildThing = IdentifiableSubclass(Thing);

// Error: Supplied parameters do not match any signature of call target.
let child = new ChildThing();

child = new ChildThing(/* id = */ 1234); // Ok

child.hello;  // Ok
child._id;    // Ok

class BadThing {
  public _id: string;
  // constructor(name: string) {}
}

// Could produce Error: Anonymous class 'C' incorrectly extends
// base class 'BadThing'. Types of property '_id' are incompatible.
// Type 'number' is not assignable to type 'string'
const BadChildThing = IdentifiableSubclass(BadThing);

That might be a longshot as a proposal for this issue, but this class of generic type propagation and errors/type checking at the callsite could in general help tremendously.

As an alternative to using a Base/Constructor interface, it might also be possible to use <T extends typeof BaseClass>, like so:

class BaseClass {
}

function IdentifiableSubclass<T extends typeof BaseClass>(SuperClass: T) {
    // Only compatibility with BaseClass can be checked here
    return class C extends SuperClass {
      constructor(id) {
        super()
      }
      public _id: number = 0;
    };
}

The <T extends typeof BaseClass> notation already accepts classes of the right typing, but cannot be used as a constructor function type, for presumably the same reason that the shape is unknown.

Anyway, that could be nice, but I'm guessing would likely be a side-effect of a much larger different proposal for callsite propagation type checking. Would still love to hear any thoughts on it though.

tejacques commented Dec 22, 2015

Is there an inherent reason why classes can't inherit from an intersection type? It seems like the entire shape is known in that scenario, so maybe support could be added? It would have to reconcile that intersection types could have different type declarations on the same properties, but on the surface still seems like it could be done (and disallow those with an error).

Another possibility that affords a much friendlier looking implementation would be to lift the typing up to the call site where the shape is known, and check the constraints there (much like templating in C++ as much as that thought probably horrifies everyone). At the declaration function you'd only be able to check the base type, but you could get an error at the callsite when you were doing something improper:

interface Base<T> {
  new(): T
}

function IdentifiableSubclass<T>(SuperClass: Base<T>) {
    // Only compatibility with Base<T> can be checked here
    return class C extends SuperClass {
      constructor(id) {
        super()
      }
      public _id: number = 0;
    };
}

class Thing {
    public hello = null;
    /* impl */
}

const ChildThing = IdentifiableSubclass(Thing);

// Error: Supplied parameters do not match any signature of call target.
let child = new ChildThing();

child = new ChildThing(/* id = */ 1234); // Ok

child.hello;  // Ok
child._id;    // Ok

class BadThing {
  public _id: string;
  // constructor(name: string) {}
}

// Could produce Error: Anonymous class 'C' incorrectly extends
// base class 'BadThing'. Types of property '_id' are incompatible.
// Type 'number' is not assignable to type 'string'
const BadChildThing = IdentifiableSubclass(BadThing);

That might be a longshot as a proposal for this issue, but this class of generic type propagation and errors/type checking at the callsite could in general help tremendously.

As an alternative to using a Base/Constructor interface, it might also be possible to use <T extends typeof BaseClass>, like so:

class BaseClass {
}

function IdentifiableSubclass<T extends typeof BaseClass>(SuperClass: T) {
    // Only compatibility with BaseClass can be checked here
    return class C extends SuperClass {
      constructor(id) {
        super()
      }
      public _id: number = 0;
    };
}

The <T extends typeof BaseClass> notation already accepts classes of the right typing, but cannot be used as a constructor function type, for presumably the same reason that the shape is unknown.

Anyway, that could be nice, but I'm guessing would likely be a side-effect of a much larger different proposal for callsite propagation type checking. Would still love to hear any thoughts on it though.

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Jan 14, 2016

Contributor

The proposal we discussed for this issue before is to introduce a new extends type operation, that is similar to intersection, but only picks the first member in case the two types had members with the same name, in addition the new operation will ignore errors to allow extending from type parameters.

Contributor

mhegazy commented Jan 14, 2016

The proposal we discussed for this issue before is to introduce a new extends type operation, that is similar to intersection, but only picks the first member in case the two types had members with the same name, in addition the new operation will ignore errors to allow extending from type parameters.

@tejacques

This comment has been minimized.

Show comment
Hide comment
@tejacques

tejacques Jan 14, 2016

Interesting. So the proposal is something like this?

interface A {
    A: number
    B: number
}

interface B {
    A: string
    B: number
    C: boolean
}

type AorB = A | B;
let aORb: AorB;
aORb.A // number | string
aORb.B // number
aORb.C // error -- can only access under typeguard on B

type AandB = A & B;
let aANDb: AandB;
aANDb.A // number & string
aANDb.B // number
aANDb.C // boolean

type AenhancedB = A || B;
let aENb: AenhancedB;
aENb.A // number
aENb.B // number
aENb.C // boolean

I used || here since it fits decently well with JavaScript semantics: a || b is a, or b if a doesn't have a value. How would it resolve more complicated types? Does it merge them recursively?

tejacques commented Jan 14, 2016

Interesting. So the proposal is something like this?

interface A {
    A: number
    B: number
}

interface B {
    A: string
    B: number
    C: boolean
}

type AorB = A | B;
let aORb: AorB;
aORb.A // number | string
aORb.B // number
aORb.C // error -- can only access under typeguard on B

type AandB = A & B;
let aANDb: AandB;
aANDb.A // number & string
aANDb.B // number
aANDb.C // boolean

type AenhancedB = A || B;
let aENb: AenhancedB;
aENb.A // number
aENb.B // number
aENb.C // boolean

I used || here since it fits decently well with JavaScript semantics: a || b is a, or b if a doesn't have a value. How would it resolve more complicated types? Does it merge them recursively?

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Jan 14, 2016

Contributor

that is correct. except that i would call it a extends b. and no there is no recursive merging. at least this is the proposal.

Contributor

mhegazy commented Jan 14, 2016

that is correct. except that i would call it a extends b. and no there is no recursive merging. at least this is the proposal.

@tejacques

This comment has been minimized.

Show comment
Hide comment
@tejacques

tejacques Jan 15, 2016

Hm, so the syntax is:

type AextendsB = A extends B;

Does this proposal apply to the existing usage of extends in generics or classes, or is it a new separate thing? Also how would the original example be implemented under the proposal?

I.E. does the new proposal allow for this example?

function enhance<T>(Superclass: new() => T) {
    return class Subclass extends Superclass {
    }
}

And the return type would be an anonymous class with the constructor of type: new () => AnonymousType extends T? Something like that?

Thanks for your work on this!

tejacques commented Jan 15, 2016

Hm, so the syntax is:

type AextendsB = A extends B;

Does this proposal apply to the existing usage of extends in generics or classes, or is it a new separate thing? Also how would the original example be implemented under the proposal?

I.E. does the new proposal allow for this example?

function enhance<T>(Superclass: new() => T) {
    return class Subclass extends Superclass {
    }
}

And the return type would be an anonymous class with the constructor of type: new () => AnonymousType extends T? Something like that?

Thanks for your work on this!

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Jan 15, 2016

Contributor

correct. that is the proposal.

Contributor

mhegazy commented Jan 15, 2016

correct. that is the proposal.

@Artazor

This comment has been minimized.

Show comment
Hide comment
@Artazor

Artazor Jan 16, 2016

Contributor

@mhegazy

Am I right that the following code will work as expected?

interface X {
    x: string;
    me(): this;
}

interface Y {
   y: string;
}

var v1: Y & X = { x: "A", y: "B", me(){ return this; }}
console.log(v1.me().x + v1.me().y) //Error, property 'y' does not exist on type 'X'

var v2: Y extends X = { x: "A", y: "B", me(){ return this; }}
console.log(v2.me().x + v2.me().y) // OK, produces "AB"
Contributor

Artazor commented Jan 16, 2016

@mhegazy

Am I right that the following code will work as expected?

interface X {
    x: string;
    me(): this;
}

interface Y {
   y: string;
}

var v1: Y & X = { x: "A", y: "B", me(){ return this; }}
console.log(v1.me().x + v1.me().y) //Error, property 'y' does not exist on type 'X'

var v2: Y extends X = { x: "A", y: "B", me(){ return this; }}
console.log(v2.me().x + v2.me().y) // OK, produces "AB"
@Artazor

This comment has been minimized.

Show comment
Hide comment
@Artazor

Artazor Jan 16, 2016

Contributor

Just related thoughts:

It would be nice to have something like 'extends' expression between object literal and an arbitrary expression in ES'Next (with appropriate typed counterpart in TS'Next)

var a = { x: 1 }
var b = { f() { return this.x } } extends a;

or more autocomplete friendly

var a = { x: 1 }
var b = extends a { 
   f() { return this.x } // benefits of autocompleting this.x
}

with the semantics of

var a = { x: 1 }
var b = {
    __proto__: a, 
    f() { return this.x }
 };

In this case we can say that if a:T1 and b:T2 then (b extends a):(T2 extends T1)

Contributor

Artazor commented Jan 16, 2016

Just related thoughts:

It would be nice to have something like 'extends' expression between object literal and an arbitrary expression in ES'Next (with appropriate typed counterpart in TS'Next)

var a = { x: 1 }
var b = { f() { return this.x } } extends a;

or more autocomplete friendly

var a = { x: 1 }
var b = extends a { 
   f() { return this.x } // benefits of autocompleting this.x
}

with the semantics of

var a = { x: 1 }
var b = {
    __proto__: a, 
    f() { return this.x }
 };

In this case we can say that if a:T1 and b:T2 then (b extends a):(T2 extends T1)

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Feb 25, 2016

I just filed #7225 which is mostly a dupe of this I see.

Having this just work:

function Mixin<T>(superclass: new() => T) => class extends superclass {
};

would allow for typed mixins, exactly what I'm looking for.

Even better is a way to refer to the return type of Mixin, or to use Mixin itself. ES2015's @@hasInstance allows objects to override the instanceof operator, and implementing it on Mixin allows for expressions like this to work:

foo instanceof Mixin

It would be nice if it worked as a type:

let m: Mixin;
interface X extends Mixin { ... }

justinfagnani commented Feb 25, 2016

I just filed #7225 which is mostly a dupe of this I see.

Having this just work:

function Mixin<T>(superclass: new() => T) => class extends superclass {
};

would allow for typed mixins, exactly what I'm looking for.

Even better is a way to refer to the return type of Mixin, or to use Mixin itself. ES2015's @@hasInstance allows objects to override the instanceof operator, and implementing it on Mixin allows for expressions like this to work:

foo instanceof Mixin

It would be nice if it worked as a type:

let m: Mixin;
interface X extends Mixin { ... }
@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Feb 25, 2016

@mhegazy what's the status of this proposal?

justinfagnani commented Feb 25, 2016

@mhegazy what's the status of this proposal?

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Feb 25, 2016

Contributor

the issue is still on the list of items to discuss in the language design meeting. we have not got to it yet.

Contributor

mhegazy commented Feb 25, 2016

the issue is still on the list of items to discuss in the language design meeting. we have not got to it yet.

@mhegazy mhegazy referenced this issue May 17, 2016

Open

Making Ember + TypeScript Ergonomic #8637

0 of 4 tasks complete

@mhegazy mhegazy changed the title from `class extends` check is too restrictive to Allow class to extend from a generic type parameter Jun 7, 2016

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jun 13, 2016

Member

Can someone write up a short summary of the proposed change and its behavior?

Member

RyanCavanaugh commented Jun 13, 2016

Can someone write up a short summary of the proposed change and its behavior?

@Artazor

This comment has been minimized.

Show comment
Hide comment
@Artazor

Artazor Jul 17, 2016

Contributor

For the sake of keyword economy, we can use extends, but I'd rather use the keyword overrides, it is more semantically correct.

The special type constructor is being proposed: T overrides U
It is not a constraint, it is a type. So you freely can write:

var a: {x: number} overrides {x: {}, y: boolean}; 
    // and get the correct { x: number, y: boolean}

As well as

var X: TExtension overrides TBase; 
  // where both TExtension and TBase are generic type params

This operator ensures that extension is correct at the instantiation time.

var a: {x: number} extends {x: string, y: boolean}; 
   // error:  number and string are not compatible   

For all actual types T and U the type T overrides U inferred exactly according to the TypeScript member overriding rules.

inspired by @jesseschalken example from the #9776

export function decorate<T>(base: new() => T }): new() => {
  barMethod(x: string): void
} overrides T {
  return class extends base {
      public barMethod(x:string):void {}
  };
}

class Foo {
    public fooMethod(x: number) { return x; }
}

class Bar {
    public barMethod(x:number):string { return 'hello'; }
} 

const FooDecorated = decorate(Foo); // Ok
const BarDecorated = decorate(Bar); // error at compile time 
                                    // (barMethod signatures are incompatible)
Contributor

Artazor commented Jul 17, 2016

For the sake of keyword economy, we can use extends, but I'd rather use the keyword overrides, it is more semantically correct.

The special type constructor is being proposed: T overrides U
It is not a constraint, it is a type. So you freely can write:

var a: {x: number} overrides {x: {}, y: boolean}; 
    // and get the correct { x: number, y: boolean}

As well as

var X: TExtension overrides TBase; 
  // where both TExtension and TBase are generic type params

This operator ensures that extension is correct at the instantiation time.

var a: {x: number} extends {x: string, y: boolean}; 
   // error:  number and string are not compatible   

For all actual types T and U the type T overrides U inferred exactly according to the TypeScript member overriding rules.

inspired by @jesseschalken example from the #9776

export function decorate<T>(base: new() => T }): new() => {
  barMethod(x: string): void
} overrides T {
  return class extends base {
      public barMethod(x:string):void {}
  };
}

class Foo {
    public fooMethod(x: number) { return x; }
}

class Bar {
    public barMethod(x:number):string { return 'hello'; }
} 

const FooDecorated = decorate(Foo); // Ok
const BarDecorated = decorate(Bar); // error at compile time 
                                    // (barMethod signatures are incompatible)
@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Aug 16, 2016

That looks pretty good.

On the name, I prefer extends because the LHS type might not actually override anything in the RHS type, and it matches the type produced by the extends clause already.

One major improvement on this, IMO, would be to not have to repeat the interface for a mixin's class expression.

justinfagnani commented Aug 16, 2016

That looks pretty good.

On the name, I prefer extends because the LHS type might not actually override anything in the RHS type, and it matches the type produced by the extends clause already.

One major improvement on this, IMO, would be to not have to repeat the interface for a mixin's class expression.

@datokrat

This comment has been minimized.

Show comment
Hide comment
@datokrat

datokrat Aug 30, 2016

Update: I read this and other threads before but now I think that the approach below was already discussed in its most relevant aspects and that I misunderstood the idea behind "overrides". Sorry.

I doubt the use of "overrides" would be a good idea. You might run into strange runtime errors overriding incompatible classes - there should at least also be a way to accomplish generic inheritance with stricter compatibility checks. An example:

interface I {
  foo: string;
}

function extend<interface T ~ I>(Base: Constructible<T>): Constructible<interface extends I, T {}> {
  return class extends Base {
    foo: string = "Hi";
  }
}

class Base1 {
    bar: number;
}

class Base2 {
    foo: number;
    bar: number;
}

interface NonConflictingBase2 {
    bar: number;
}

const Extended1 = extend(Base1); //ok
//note that in the following case, the compiler should infer the complete class as implicit type parameter
//instead of {} to receive this error. So implicit type inference should ignore failing ~ constraints.
const Extended2 = extend(Base2); //error: typeof Base2 does not satisfy constraint
const Overridden2 = extend<NonConflictingBase2>(Base2); //ok

This involves three new features:

  1. The "interface" type parameter constrains T to be an interface or class.
  2. The ~ operator requires compatibility in the manner of class extension.
  3. anonymous interfaces as return type. Optional but this could simplify many use cases significantly.

datokrat commented Aug 30, 2016

Update: I read this and other threads before but now I think that the approach below was already discussed in its most relevant aspects and that I misunderstood the idea behind "overrides". Sorry.

I doubt the use of "overrides" would be a good idea. You might run into strange runtime errors overriding incompatible classes - there should at least also be a way to accomplish generic inheritance with stricter compatibility checks. An example:

interface I {
  foo: string;
}

function extend<interface T ~ I>(Base: Constructible<T>): Constructible<interface extends I, T {}> {
  return class extends Base {
    foo: string = "Hi";
  }
}

class Base1 {
    bar: number;
}

class Base2 {
    foo: number;
    bar: number;
}

interface NonConflictingBase2 {
    bar: number;
}

const Extended1 = extend(Base1); //ok
//note that in the following case, the compiler should infer the complete class as implicit type parameter
//instead of {} to receive this error. So implicit type inference should ignore failing ~ constraints.
const Extended2 = extend(Base2); //error: typeof Base2 does not satisfy constraint
const Overridden2 = extend<NonConflictingBase2>(Base2); //ok

This involves three new features:

  1. The "interface" type parameter constrains T to be an interface or class.
  2. The ~ operator requires compatibility in the manner of class extension.
  3. anonymous interfaces as return type. Optional but this could simplify many use cases significantly.
@vidartf

This comment has been minimized.

Show comment
Hide comment
@vidartf

vidartf Sep 14, 2016

I'm +1 on this.

@RyanCavanaugh, @ahejlsberg: As far as I read this, the proposed solution is to somehow specify to the compiler that T in the code below will be a class or interface type (and maybe specify something of its type), and then move this check to when T is specified (when the function is called).

// This currently checks that T is not class or interface, and gives error
function Mixin<T /* possibly add some constraint here */>(superclass: new() => T) {
  return class extends superclass {};
}

// If the check was performed here, it would see that T resolves to Object which is a class
const Mixed = Mixin(Object);

Or at the very least it would be nice if you could add the relevant features for adding typings to such javascript code.

vidartf commented Sep 14, 2016

I'm +1 on this.

@RyanCavanaugh, @ahejlsberg: As far as I read this, the proposed solution is to somehow specify to the compiler that T in the code below will be a class or interface type (and maybe specify something of its type), and then move this check to when T is specified (when the function is called).

// This currently checks that T is not class or interface, and gives error
function Mixin<T /* possibly add some constraint here */>(superclass: new() => T) {
  return class extends superclass {};
}

// If the check was performed here, it would see that T resolves to Object which is a class
const Mixed = Mixin(Object);

Or at the very least it would be nice if you could add the relevant features for adding typings to such javascript code.

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Sep 14, 2016

Contributor

@RyanCavanaugh, @ahejlsberg: As far as I read this, the proposed solution is to somehow specify to the compiler that T in the code below will be a class or interface type (and maybe specify something of its type), and then move this check to when T is specified (when the function is called).

Not perniciously. the proposal is that there is a new type operator called T extends U. The return type of your function would be along the lines of {} extends T, (where {} refers to the shape of the class expression). There are no errors reported if the class has a property conflicting with the value of T at use site. but the resulting type would be generated by taking T and overlaying the properties of your class expression.

Contributor

mhegazy commented Sep 14, 2016

@RyanCavanaugh, @ahejlsberg: As far as I read this, the proposed solution is to somehow specify to the compiler that T in the code below will be a class or interface type (and maybe specify something of its type), and then move this check to when T is specified (when the function is called).

Not perniciously. the proposal is that there is a new type operator called T extends U. The return type of your function would be along the lines of {} extends T, (where {} refers to the shape of the class expression). There are no errors reported if the class has a property conflicting with the value of T at use site. but the resulting type would be generated by taking T and overlaying the properties of your class expression.

@QuantumInformation

This comment has been minimized.

Show comment
Hide comment
@QuantumInformation

QuantumInformation Nov 17, 2016

Hi, I gave a talk on TypeScript at the latest ember London meetup, how close are we to having a nice workflow with ts+ember so I can report back?

QuantumInformation commented Nov 17, 2016

Hi, I gave a talk on TypeScript at the latest ember London meetup, how close are we to having a nice workflow with ts+ember so I can report back?

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Nov 18, 2016

Are there any issues open for the extends type operator?

justinfagnani commented Nov 18, 2016

Are there any issues open for the extends type operator?

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Nov 18, 2016

Contributor

Are there any issues open for the extends type operator?

this is the issue tracking this work.

Contributor

mhegazy commented Nov 18, 2016

Are there any issues open for the extends type operator?

this is the issue tracking this work.

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Jan 4, 2017

I wanted to check in and see if there was any possibility of progress here, since the type system got some nice upgrades recently.

This is the closest I've ever been able to come at typing ES6 mixins correctly, which unfortunately still has a pretty high cost on both the definition and use sides:

definition:

interface Constructable<T> {
  new (...args: any[]): T;
}

interface Base {}

interface HasFoo {
  foo(): string;
}

interface HasBar {
  bar(): number;
}

let M1 = <T extends Base>(superclass: Constructable<T>) => class extends (<Constructable<Base>>superclass) {
  foo() { return 'a string'; }
}

let M2 = <T extends Base>(superclass: Constructable<T>) => class extends (<Constructable<Base>>superclass) {
  bar() { return 42; }
}

use:

// without this interface we can't inform the typesystem that C1 has both foo() and bar()
interface C extends HasFoo, HasBar, Constructable<C> {}

// without the cast to C, C1 doesn't appear to have foo()
class C1 extends (<C>M2(M1(Object))) {
  baz() { return true; }
}

let c = new C1();
console.log(c.foo(), c.bar(), c.baz());

Not only would the extend type operator be helpful for shortening this like @Artazor's example, but it'd be much better if we could omit the extra interface declaration and get at the inferred return type of the mixin.

Maybe something like:

let MixinFoo = <T>(superclass: new() => T) => class extends superclass {
  foo() { return 'a string'; }
}
const ObjectWithFoo = MixinFoo(Object);
type HasFoo = typeof ObjectWithFoo;

function useFoo(o: HasFoo) {...}

justinfagnani commented Jan 4, 2017

I wanted to check in and see if there was any possibility of progress here, since the type system got some nice upgrades recently.

This is the closest I've ever been able to come at typing ES6 mixins correctly, which unfortunately still has a pretty high cost on both the definition and use sides:

definition:

interface Constructable<T> {
  new (...args: any[]): T;
}

interface Base {}

interface HasFoo {
  foo(): string;
}

interface HasBar {
  bar(): number;
}

let M1 = <T extends Base>(superclass: Constructable<T>) => class extends (<Constructable<Base>>superclass) {
  foo() { return 'a string'; }
}

let M2 = <T extends Base>(superclass: Constructable<T>) => class extends (<Constructable<Base>>superclass) {
  bar() { return 42; }
}

use:

// without this interface we can't inform the typesystem that C1 has both foo() and bar()
interface C extends HasFoo, HasBar, Constructable<C> {}

// without the cast to C, C1 doesn't appear to have foo()
class C1 extends (<C>M2(M1(Object))) {
  baz() { return true; }
}

let c = new C1();
console.log(c.foo(), c.bar(), c.baz());

Not only would the extend type operator be helpful for shortening this like @Artazor's example, but it'd be much better if we could omit the extra interface declaration and get at the inferred return type of the mixin.

Maybe something like:

let MixinFoo = <T>(superclass: new() => T) => class extends superclass {
  foo() { return 'a string'; }
}
const ObjectWithFoo = MixinFoo(Object);
type HasFoo = typeof ObjectWithFoo;

function useFoo(o: HasFoo) {...}
@shlomiassaf

This comment has been minimized.

Show comment
Hide comment
@shlomiassaf

shlomiassaf Jan 11, 2017

This is the closest I got to mixins, I have both Instance & Static member reflected in the new type.
The only drawback is not being able to extend the type created.

// START - MIXIN UTIL
export interface Type<T> extends Function { new (...args: any[]): T; }
export type Tixin<BASE, MIXIN> = BASE & MIXIN;
export function Tixin<TBASE, CBASE, TMIXIN, CMIXIN, SMIXIN>(base: CBASE & Type<TBASE>, mixin: CMIXIN & Type<TMIXIN>): Type<TBASE & TMIXIN> & CMIXIN & CBASE {
  // basic mixin fn, copy instance values and static values.
  Object.getOwnPropertyNames(mixin.prototype)
    .forEach(name => base.prototype[name] = mixin.prototype[name]);

  Object.getOwnPropertyNames(mixin).forEach(name => {
    if (!base.hasOwnProperty(name)) {
      base[name] = mixin[name];
    }
  });
  return base as any;
}
// END - MIXIN UTIL


// START - USAGE DEMO

/**
 * out base class, has static and instance members.
 */
class User_ {
  id: number;
  username: string;
  age: number;

  static getOne(): number {
    return 1;
  }
}

/**
 * A class to mixin into User_
 * Also has static and instance members.
 */
class Resource {
  age2: number;

  add(num: number): number {
    return num + this.age2;
  }

  static getTwo(): number {
    return 2;
  }
}

// these are the exported value and type (should mimic class that has both type and value)
export const User = Tixin(User_, Resource);
export type User = Tixin<User_, Resource>;


// now lets see in action:
let user: User = new User();
user.username = 'jd';
user.age = 30;
user.age2 = 40;
console.log(`This should be 70: ${user.add(user.age)}`);

console.log(`This should be 3: ${User.getOne() + User.getTwo()}`);

// NO TYPE ERRORS TILL THIS POINT, NO RUNTIME ERRORS TILL THIS POINT.


// ERROR IN CODE FROM THIS POINT:

class XYZ extends User { // THIS CAUSE THE ERROR: Type 'Type<User_ & Resource> & typeof Resource & typeof User_' is not a constructor function type.
  // override behavior.
  add(num: number): number {
    return this.age2 - num;
  }
}

// YET IN RUNTIME WORKS FINE:
let user2: XYZ = new XYZ();
user2.username = 'jd';
user2.age = 30;
user2.age2 = 40;
console.log(`This should be 10: ${user2.add(user2.age)}`); // 10 instead of 70
console.log(`This should be 3: ${XYZ.getOne() + XYZ.getTwo()}`); // no change

Do you expect this new proposal to solve this issue? is it the same problem?

shlomiassaf commented Jan 11, 2017

This is the closest I got to mixins, I have both Instance & Static member reflected in the new type.
The only drawback is not being able to extend the type created.

// START - MIXIN UTIL
export interface Type<T> extends Function { new (...args: any[]): T; }
export type Tixin<BASE, MIXIN> = BASE & MIXIN;
export function Tixin<TBASE, CBASE, TMIXIN, CMIXIN, SMIXIN>(base: CBASE & Type<TBASE>, mixin: CMIXIN & Type<TMIXIN>): Type<TBASE & TMIXIN> & CMIXIN & CBASE {
  // basic mixin fn, copy instance values and static values.
  Object.getOwnPropertyNames(mixin.prototype)
    .forEach(name => base.prototype[name] = mixin.prototype[name]);

  Object.getOwnPropertyNames(mixin).forEach(name => {
    if (!base.hasOwnProperty(name)) {
      base[name] = mixin[name];
    }
  });
  return base as any;
}
// END - MIXIN UTIL


// START - USAGE DEMO

/**
 * out base class, has static and instance members.
 */
class User_ {
  id: number;
  username: string;
  age: number;

  static getOne(): number {
    return 1;
  }
}

/**
 * A class to mixin into User_
 * Also has static and instance members.
 */
class Resource {
  age2: number;

  add(num: number): number {
    return num + this.age2;
  }

  static getTwo(): number {
    return 2;
  }
}

// these are the exported value and type (should mimic class that has both type and value)
export const User = Tixin(User_, Resource);
export type User = Tixin<User_, Resource>;


// now lets see in action:
let user: User = new User();
user.username = 'jd';
user.age = 30;
user.age2 = 40;
console.log(`This should be 70: ${user.add(user.age)}`);

console.log(`This should be 3: ${User.getOne() + User.getTwo()}`);

// NO TYPE ERRORS TILL THIS POINT, NO RUNTIME ERRORS TILL THIS POINT.


// ERROR IN CODE FROM THIS POINT:

class XYZ extends User { // THIS CAUSE THE ERROR: Type 'Type<User_ & Resource> & typeof Resource & typeof User_' is not a constructor function type.
  // override behavior.
  add(num: number): number {
    return this.age2 - num;
  }
}

// YET IN RUNTIME WORKS FINE:
let user2: XYZ = new XYZ();
user2.username = 'jd';
user2.age = 30;
user2.age2 = 40;
console.log(`This should be 10: ${user2.add(user2.age)}`); // 10 instead of 70
console.log(`This should be 3: ${XYZ.getOne() + XYZ.getTwo()}`); // no change

Do you expect this new proposal to solve this issue? is it the same problem?

@shlomiassaf

This comment has been minimized.

Show comment
Hide comment
@shlomiassaf

shlomiassaf Jan 11, 2017

This proposal doesn't seem to be in the Roadmap, not for 2.2 or later...

Is there something the team can share about the progress?

shlomiassaf commented Jan 11, 2017

This proposal doesn't seem to be in the Roadmap, not for 2.2 or later...

Is there something the team can share about the progress?

@shlomiassaf

This comment has been minimized.

Show comment
Hide comment
@shlomiassaf

shlomiassaf Jan 11, 2017

Another issue that might be related is chaining of generated types

Using the Tixin type & function from the example above:

class User_ {
  id: number;
  firstName: string;
  lastName: string;
}

class FullName {
  middleName: string;

  get fullName(): string {
    return `${this['firstName']}${this.middleName ? ' ' + this.middleName : ''} ${this['lastName']}`;
  }

  static createId(): number {
    // a shady id generator.
    return Date.now();
  }

}
export const User = Mixin(User_, FullName);
export type User = Mixin<User_, FullName>;

//  SO FAR SO GOOD... now create another mixin

class OtherMixin {
  otherName: string;

  static otherThing(): number {
    return 5;
  }
}

// using the new User type we have:
export const UserNumber2 = Mixin(User, OtherMixin);

We get this error:

TS2453: The type argument for type parameter 'TBASE' cannot be inferred from the usage. Consider specifying the type arguments explicitly.  

Type argument candidate 'FullName' is not a valid type argument because it is not a supertype of candidate 'User_'.     Property 'middleName' is missing in type 'User_

It might be related to this or not I can't tell :)

shlomiassaf commented Jan 11, 2017

Another issue that might be related is chaining of generated types

Using the Tixin type & function from the example above:

class User_ {
  id: number;
  firstName: string;
  lastName: string;
}

class FullName {
  middleName: string;

  get fullName(): string {
    return `${this['firstName']}${this.middleName ? ' ' + this.middleName : ''} ${this['lastName']}`;
  }

  static createId(): number {
    // a shady id generator.
    return Date.now();
  }

}
export const User = Mixin(User_, FullName);
export type User = Mixin<User_, FullName>;

//  SO FAR SO GOOD... now create another mixin

class OtherMixin {
  otherName: string;

  static otherThing(): number {
    return 5;
  }
}

// using the new User type we have:
export const UserNumber2 = Mixin(User, OtherMixin);

We get this error:

TS2453: The type argument for type parameter 'TBASE' cannot be inferred from the usage. Consider specifying the type arguments explicitly.  

Type argument candidate 'FullName' is not a valid type argument because it is not a supertype of candidate 'User_'.     Property 'middleName' is missing in type 'User_

It might be related to this or not I can't tell :)

@zerovox

This comment has been minimized.

Show comment
Hide comment
@zerovox

zerovox Jan 20, 2017

Just ran into this today. Have some code, similar to the mixin examples above:

export interface DelegateConstructor<T> {
  new (delegate: T): T;
}

export function autoDelegate<T, K extends keyof T>(...keys: K[]): DelegateConstructor<Pick<T, K>> {
  return (function () {
    function DelegatingClass(delegate: T) {
      const that = (this as any);
      that.delegate = delegate;
    }

    for (let key in keys) {
      DelegatingClass.prototype[key] = function() {
        return this.delegate[key].apply(this.delegate, arguments);
      }
    }

    return DelegatingClass;
  }()) as any;
}

Usage:

type DelegatedMethods = 'entries' | 'keys' | 'values' | 'forEach' | 'has' | 'size';

class ImmutableSetImpl<T> extends autoDelegate<Set<T>, DelegatedMethods>('entries', 'keys', 'values', 'forEach', 'has', 'size') {
  private delegate: Set<T>;

  constructor(delegate: Set<T>) {
    super(delegate);
    this.delegate = delegate;
  }

  [Symbol.iterator](): Iterator<T> {
    return this.delegate[Symbol.iterator]();
  }

  with(value: T): ImmutableSet<T> {
    const clone = new Set<T>(this.delegate);
    clone.add(value);
    return new ImmutableSetImpl(clone);
  }
}

But, we get the errors:

error TS2509: Base constructor return type 'Pick<Set<T>, DelegatedMethods>' is not a class or interface type.

zerovox commented Jan 20, 2017

Just ran into this today. Have some code, similar to the mixin examples above:

export interface DelegateConstructor<T> {
  new (delegate: T): T;
}

export function autoDelegate<T, K extends keyof T>(...keys: K[]): DelegateConstructor<Pick<T, K>> {
  return (function () {
    function DelegatingClass(delegate: T) {
      const that = (this as any);
      that.delegate = delegate;
    }

    for (let key in keys) {
      DelegatingClass.prototype[key] = function() {
        return this.delegate[key].apply(this.delegate, arguments);
      }
    }

    return DelegatingClass;
  }()) as any;
}

Usage:

type DelegatedMethods = 'entries' | 'keys' | 'values' | 'forEach' | 'has' | 'size';

class ImmutableSetImpl<T> extends autoDelegate<Set<T>, DelegatedMethods>('entries', 'keys', 'values', 'forEach', 'has', 'size') {
  private delegate: Set<T>;

  constructor(delegate: Set<T>) {
    super(delegate);
    this.delegate = delegate;
  }

  [Symbol.iterator](): Iterator<T> {
    return this.delegate[Symbol.iterator]();
  }

  with(value: T): ImmutableSet<T> {
    const clone = new Set<T>(this.delegate);
    clone.add(value);
    return new ImmutableSetImpl(clone);
  }
}

But, we get the errors:

error TS2509: Base constructor return type 'Pick<Set<T>, DelegatedMethods>' is not a class or interface type.
@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Jan 20, 2017

Member

@wycats The features implemented in #13604 should help a lot here.

Member

ahejlsberg commented Jan 20, 2017

@wycats The features implemented in #13604 should help a lot here.

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Jan 20, 2017

@ahejlsberg that's a huge step forward! :) Any word on the extends operator?

justinfagnani commented Jan 20, 2017

@ahejlsberg that's a huge step forward! :) Any word on the extends operator?

@atrauzzi

This comment has been minimized.

Show comment
Hide comment
@atrauzzi

atrauzzi Jan 25, 2017

Would absolutely love to see this happen.

I have a type that needs a bit of a kludge to get by currently because I can't extend a generic type in its root ancestor type.

atrauzzi commented Jan 25, 2017

Would absolutely love to see this happen.

I have a type that needs a bit of a kludge to get by currently because I can't extend a generic type in its root ancestor type.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Jan 30, 2017

Member

Mixin classes as implemented by #13743 are now in master branch.

Member

ahejlsberg commented Jan 30, 2017

Mixin classes as implemented by #13743 are now in master branch.

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