microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.31k stars 12.39k forks source link

Allow class to extend from a generic type parameter #4890

Closed wycats closed 7 years ago

wycats commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

correct. that is the proposal.

Artazor commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 8 years ago

@mhegazy what's the status of this proposal?

mhegazy commented 8 years ago

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

RyanCavanaugh commented 8 years ago

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

Artazor commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 7 years ago

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 commented 7 years ago

Are there any issues open for the extends type operator?

mhegazy commented 7 years ago

Are there any issues open for the extends type operator?

this is the issue tracking this work.

justinfagnani commented 7 years ago

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 commented 7 years ago

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 7 years ago

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 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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

justinfagnani commented 7 years ago

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

atrauzzi commented 7 years ago

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 commented 7 years ago

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

renatoaraujoc commented 7 months ago

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

Impressive how a post from 2015 has just saved my life, thank you very much for this!

With this:

static withPathsStructure<TFSPaths extends FileSystemPathsStructure>() {
    class IFileSystemAsClass extends (<Constructor<IFileSystem>>(
        (<unknown>FileSystemService)
    )) {}

    return IFileSystemAsClass as Constructor<
        IFileSystemAsClass & FileSystemService<TFSPaths>
    >;
}

I can do this:

/**
* Now `ApiFileSystemService` inherits `FileSystemService` plus the interface `IFileSystem`.
*/
export class ApiFileSystemService extends FileSystemService.withPathsStructure<{
    uploads: true;
    acts: {
        documentConvertionSandbox: true;
        texts: true;
        attachments: true;
    };
}>() {}

And in NestJS I can do:

providers: [
    {
        provide: ApiFileSystemService,
        useExisting: FileSystemService
    }
]

And this happens :D Screenshot 2024-02-13 at 20 49 31

Thank you soooo much! <3

the-homeless-god commented 2 months ago

For cases when u need to extend it more:



// JFYI: Universal constructor
interface Constructor<T> {
    new (...args: any[]): T;
}

function SwitchableSubclass<Animal extends Duck | Dog>(SuperClass: Constructor<T>, animal: Animal) {
    // JFYI: I'm thinking how to get rid of intersection to support |
    class C extends (<Constructor<Duck & Dog>>SuperClass) {
       getName() {
           super.getName();
           console.info(animal);
        }
    }

    return <Constructor<T>>((<unknown>(<Animal>(<unknown>C))));
}

// JFYI: Extender for inheritance
export function useExtender<Animal extends Duck | Dog>(animal: Animal, SuperClass: Constructor<T>) {
    return SwitchableSubclass<Animal>(SuperClass, animal);
}

const immutableDog = new Dog();
const immutableDuck = new Duck();

class Bulldog extends useExtender<Dog>(immutableDog, Dog) {
  // JFYI: Mutated version of Dog
}

class Grayduck extends useExtender<Duck>(immutableDuck, Duck) {
  // JFYI: Mutated version of Duck
}

const mutableDog = new Bulldog();
const mutableDuck = new Grayduck();