You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Allow type parameters in base class expressions #26542
typescript type parameters in base class expressions
Suggestion
Type parameters in base class expressions.
Use Cases and Examples
Let's start with a simple mixin [playground] (borrowed from proposal #13743):
typeConstructor<T={}>=new(...args: any[])=>T;classPoint{constructor(publicreadonlyx: number,publicreadonlyy: number,){}}// Tagged is a mixin with a type parameter.constTagged=<T,BextendsConstructor=B>(Base: B)=>classextendsBase{getTag(): T{// Just imagine I have a function that returns objects,// and I want to case them to T.return{}asT}}classTaggedPointextendsTagged(Point){}
But if I want to pass a derived class's type parameter as that argument, I cannot [playground]:
classUsesTheTag<T>extendsTagged<T>(Point){// ^// error: Base class expressions cannot reference// class type parameters.useTag(){consttag: T=this.getTag()}}
Checklist
My suggestion meets these guidelines:
This wouldn't be a breaking change in existing TypeScript / JavaScript code
This wouldn't change the runtime behavior of existing JavaScript code
This could be implemented without emitting different JS based on the types of the expressions
This isn't a runtime feature (e.g. new expression-level syntax)
Footnotes
Note, this requires the default type argument = B shown above, but I have never seen it used in documentation, Stack Overflow, or blogs on the issue of TypeScript mixins. Is that because TypeScript is not actually deducing the type parameter from the function argument? ↩
typeConstructor<T={}>=new(...args: any[])=>T;classPoint{constructor(publicreadonlyx: number,publicreadonlyy: number,){}}constTagged=(Base: Constructor)=>class<T>extendsBase{getTag(): T{// Just imagine I have a function that returns objects,// and I want to case them to T.return{}asT}};classUsesTheTag<T>extendsTagged(Point)<T>{useTag(): T{consttag: T=this.getTag()returntag}}
Oops, my code above doesn't actually work because UsesTheTag<T> isn't seen as a subtype of Point. Never mind. This is what I'd like to write, but it gives an error:
typeConstructor<T={}>=new(...args: any[])=>T;classPoint{constructor(publicreadonlyx: number,publicreadonlyy: number,){}}constTagged=<BextendsConstructor>(Base: B)=>// Error: A mixin class must have a constructor// with a single rest parameter of type 'any[]'.class<T>extendsBase{getTag(): T{// Just imagine I have a function that returns objects,// and I want to case them to T.return{}asT}};classUsesTheTag<T>extendsTagged(Point){useTag(): T{consttag: T=this.getTag()console.log(this.x);returntag}}
Why aren't mixin classes allowed to have type parameters?
Something else that confuses me, why does this first declaration work but not the second? Why that error at that source location?
constTagged=(Base: Constructor)=>class<T>extendsBase{getTag(): T{// Just imagine I have a function that returns objects,// and I want to case them to T.return{}asT}}constTagged=<BextendsConstructor>(Base: B)=>class<T>extendsBase{//~~~~~// error: A mixin class must have a constructor with a single rest parameter of type 'any[]'.getTag(): T{// Just imagine I have a function that returns objects,// and I want to case them to T.return{}asT}}
I think I answered my own question: the original superclass could be generic, and any type arguments in the extends clause are passed to the original superclass, not to the mixin. The mixin is not allowed to have type parameters because there would be no way to pass type arguments. Clearly someone thought about this but didn't see fit to write any documentation. ☹️ Hopefully this thread will serve as documentation for the next person to search for the error message.
I think we're getting off track. Here is the fixed version of what you gave earlier, in which I can use the facilities of both Tagged and Point on the final type. The only piece you forgot was to pass Point as the type argument to Constructor (playground).
typeConstructor<T={}>=new(...args: any[])=>T;classPoint{constructor(publicreadonlyx: number,publicreadonlyy: number,){}}constTagged=(Base: Constructor<Point>)=>class<T>extendsBase{getTag(): T{// I can use the facilities of the base class// (in this case, Point) because I declared that `Base`// is a `Constructor` of that class.constx: number=this.x// Constructions of my class assert the type that I return,// so that they get type safety when working with the// objects I return.return{tag: 'it'}asanyasT}};classUsesTheTag<T>extendsTagged(Point)<T>{useTag(): T{// I can use the facilities of the base class (Tagged)// and *its* base class (Point).consttag: T=this.getTag()constx: number=this.xreturntag}}// When I construct an object, I declare the type that its// `getTag` and `useTag` methods return.consttp=newUsesTheTag<{tag: string}>(1,2)// When I call `getTag` or `useTag`, TypeScript checks that// I use the return value in accordance with the tag type// I declared.consttag=tp.getTag()console.log(tag.tag)constsameTag=tp.useTag()console.log(tag.tag)// `tp` should also be a Point. These should work, just as they// did within the implementation of `getTag` and `useTag`.constx: number=tp.xconsty: number=tp.y
I'm going to consider my original question answered. Thanks for the help! In this case, a type parameter in a base class expression is not what I wanted; I wanted a type parameter in the class expression returned by the mixin.
There is a follow-up question that I think brushes up against the higher-kinded types limitation that has been documented elsewhere. I haven't seen a well put-together example, though, so I'm going to take this opportunity to try to contribute my own (playground).
typeConstructor<T={}>=new(...args: any[])=>T;// The base class.classPoint{constructor(publicreadonlyx: number,publicreadonlyy: number,){}}// An extended base class with additional facilities.classWeightedPointextendsPoint{publicreadonlyweight: number=1}// My mixin just needs the facilities of the base class.constSummedSquared=(Base: Constructor<Point>)=>classextendsBase{getSumOfSquares(): number{returnthis.x*this.x+this.y*this.x}};// My final subtype expects the extended base class// because it needs those additional facilities,// but it also wants the mixin applied.classSummedSquaredWeightedPointextendsSummedSquared(WeightedPoint){getWeightedSumOfSquares(): number{// I want to use the facilities of the base class of// the base class (`WeightPointed`), but TypeScript only// knows that the base class is a `Point` because that is// what the mixin declared.return(this.x*this.x*this.weight+this.y*this.y*this.weight// ~~~~~~// error: Property 'weight' does not exist on type 'SummedSquaredWeightedPoint'.)}}
import*asmongoosefrom'mongoose';exportdeclaretypeInstanceType<T>=T&mongoose.Document;exportdeclaretypeModelType<T,typeofT>=mongoose.Model<InstanceType<T>>&typeofT;declarevar_Model1: <T>(t?: T)=>mongoose.Model<InstanceType<T>>;exportdeclareclassModel<T,DocOmit={}>extends_Model1(){createdAt?: Date;updatedAt?: Date;}typeFooModelType=ModelType<Foo,typeofFoo>;classFooextendsModel<Foo>{staticasyncmethod(){//findOne is defined in mongoose Model // interface Model<T extends Document> extends NodeJS.EventEmitter, ModelProperties { // findOne(conditions?: any,// callback?: (err: any, res: T | null) => void): DocumentQuery<T | null, T>;// }letrs=awaitthis.findOne();//and now the result is mongoose.Document//but i want to get InstanceType<Foo> letself=thisasFooModelType;letrs2=awaitself.findOne();//what i need is //class Model<T, DocOmit={}> extends _Model1<T>() {}}}
Activity
mattmccutchen commentedon Aug 20, 2018
This alternative is working for me (playground):RyanCavanaugh commentedon Aug 20, 2018
See #26154 - trying to use an instance type parameter in a base class expression is a conceptual error
mattmccutchen commentedon Aug 20, 2018
Oops, my code above doesn't actually work because
UsesTheTag<T>isn't seen as a subtype ofPoint. Never mind. This is what I'd like to write, but it gives an error:Why aren't mixin classes allowed to have type parameters?
thejohnfreeman commentedon Aug 20, 2018
Something else that confuses me, why does this first declaration work but not the second? Why that error at that source location?
mattmccutchen commentedon Aug 20, 2018
I think I answered my own question: the original superclass could be generic, and any type arguments in the extends clause are passed to the original superclass, not to the mixin. The mixin is not allowed to have type parameters because there would be no way to pass type arguments. Clearly someone thought about this but didn't see fit to write any documentation.☹️ Hopefully this thread will serve as documentation for the next person to search for the error message.
So if we want a derived class that passes a type argument to a mixin, we'll have to go with this approach.
thejohnfreeman commentedon Aug 20, 2018
I think we're getting off track. Here is the fixed version of what you gave earlier, in which I can use the facilities of both
TaggedandPointon the final type. The only piece you forgot was to passPointas the type argument toConstructor(playground).I'm going to consider my original question answered. Thanks for the help! In this case, a type parameter in a base class expression is not what I wanted; I wanted a type parameter in the class expression returned by the mixin.
There is a follow-up question that I think brushes up against the higher-kinded types limitation that has been documented elsewhere. I haven't seen a well put-together example, though, so I'm going to take this opportunity to try to contribute my own (playground).
admosity commentedon Oct 12, 2018
John Freeman (@thejohnfreeman) This playground solves your follow-up question.
Before:
After:
Pulls the types quite nicely:
aoi-umi commentedon Dec 3, 2018
i have the same question