-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
Hey @wycats, I assume you meant 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? |
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. |
@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 So, in the 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 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). |
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 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 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. |
The proposal we discussed for this issue before is to introduce a new |
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 |
that is correct. except that i would call it |
Hm, so the syntax is: type AextendsB = A extends B; Does this proposal apply to the existing usage of 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: Thanks for your work on this! |
correct. that is the proposal. |
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" |
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 |
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 foo instanceof Mixin It would be nice if it worked as a type: let m: Mixin;
interface X extends Mixin { ... } |
@mhegazy what's the status of this proposal? |
the issue is still on the list of items to discuss in the language design meeting. we have not got to it yet. |
class extends
check is too restrictive
Can someone write up a short summary of the proposed change and its behavior? |
For the sake of keyword economy, we can use The special type constructor is being proposed: 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 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) |
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? |
Are there any issues open for the |
this is the issue tracking this work. |
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 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) {...} |
This is the closest I got to mixins, I have both Instance & Static member reflected in the new type. // 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? |
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? |
Another issue that might be related is chaining of generated types Using the 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:
It might be related to this or not I can't tell :) |
Just ran into this today. Have some code, similar to the mixin examples above:
Usage:
But, we get the errors:
|
@ahejlsberg that's a huge step forward! :) Any word on the |
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. |
Mixin classes as implemented by #13743 are now in master branch. |
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
}
] Thank you soooo much! <3 |
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(); |
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
The text was updated successfully, but these errors were encountered: