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

Allow class to extend from a generic type parameter #4890

Closed
wycats opened this issue Sep 20, 2015 · 34 comments · Fixed by #13743
Closed

Allow class to extend from a generic type parameter #4890

wycats opened this issue Sep 20, 2015 · 34 comments · Fixed by #13743
Labels
Fixed A PR has been merged for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@wycats
Copy link

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
Copy link
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?

@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Sep 21, 2015
@DanielRosenwasser
Copy link
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.

@ahejlsberg
Copy link
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).

@mhegazy mhegazy added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed In Discussion Not yet reached consensus labels Sep 21, 2015
@tejacques
Copy link

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 mhegazy added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jan 14, 2016
@mhegazy
Copy link
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
Copy link

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
Copy link
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
Copy link

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
Copy link
Contributor

mhegazy commented Jan 15, 2016

correct. that is the proposal.

@Artazor
Copy link
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
Copy link
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
Copy link

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
Copy link

@mhegazy what's the status of this proposal?

@mhegazy
Copy link
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 changed the title class extends check is too restrictive Allow class to extend from a generic type parameter Jun 7, 2016
@RyanCavanaugh
Copy link
Member

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

@Artazor
Copy link
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)

@quantuminformation
Copy link

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
Copy link

Are there any issues open for the extends type operator?

@mhegazy
Copy link
Contributor

mhegazy commented Nov 18, 2016

Are there any issues open for the extends type operator?

this is the issue tracking this work.

@justinfagnani
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link
Member

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

@justinfagnani
Copy link

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

@atrauzzi
Copy link

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
Copy link
Member

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

@renatoaraujoc
Copy link

@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
Copy link

the-homeless-god commented Jul 2, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed A PR has been merged for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.