-
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 type parameters in base class expressions #26542
Comments
type Constructor<T = {}> = new (...args: any[]) => T;
class Point {
constructor(
public readonly x: number,
public readonly y: number,
) {}
}
const Tagged = (Base: Constructor) =>
class <T> extends Base {
getTag(): T {
// Just imagine I have a function that returns objects,
// and I want to case them to T.
return {} as T
}
};
class UsesTheTag<T> extends Tagged(Point)<T> {
useTag(): T {
const tag: T = this.getTag()
return tag
}
} |
See #26154 - trying to use an instance type parameter in a base class expression is a conceptual error |
Oops, my code above doesn't actually work because type Constructor<T = {}> = new (...args: any[]) => T;
class Point {
constructor(
public readonly x: number,
public readonly y: number,
) {}
}
const Tagged = <B extends Constructor>(Base: B) =>
// Error: A mixin class must have a constructor
// with a single rest parameter of type 'any[]'.
class <T> extends Base {
getTag(): T {
// Just imagine I have a function that returns objects,
// and I want to case them to T.
return {} as T
}
};
class UsesTheTag<T> extends Tagged(Point) {
useTag(): T {
const tag: T = this.getTag()
console.log(this.x);
return tag
}
} 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? const Tagged = (Base: Constructor) =>
class <T> extends Base {
getTag(): T {
// Just imagine I have a function that returns objects,
// and I want to case them to T.
return {} as T
}
}
const Tagged = <B extends Constructor> (Base: B) =>
class <T> extends Base {
//~~~~~
// 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 {} as T
}
} |
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. type Constructor<T = {}> = new (...args: any[]) => T;
class Point<U> {
constructor(
public readonly x: U,
public readonly y: U,
) {}
}
const Tagged = <B extends Constructor>(Base: B) =>
class extends Base {
getTag(): string {
return {} as string
}
};
class UsesTheTag<U> extends Tagged(Point)<U> {
useTag(): string {
const tag: string = this.getTag()
console.log(this.x);
return tag
}
} So if we want a derived class that passes a type argument to a mixin, we'll have to go with this approach. |
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 type Constructor<T = {}> = new (...args: any[]) => T;
class Point {
constructor(
public readonly x: number,
public readonly y: number,
) {}
}
const Tagged = (Base: Constructor<Point>) =>
class <T> extends Base {
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.
const x: 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' } as any as T
}
};
class UsesTheTag<T> extends Tagged(Point)<T> {
useTag(): T {
// I can use the facilities of the base class (Tagged)
// and *its* base class (Point).
const tag: T = this.getTag()
const x: number = this.x
return tag
}
}
// When I construct an object, I declare the type that its
// `getTag` and `useTag` methods return.
const tp = new UsesTheTag<{ 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.
const tag = tp.getTag()
console.log(tag.tag)
const sameTag = 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`.
const x: number = tp.x
const y: 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). type Constructor<T = {}> = new (...args: any[]) => T;
// The base class.
class Point {
constructor (
public readonly x: number,
public readonly y: number,
) {}
}
// An extended base class with additional facilities.
class WeightedPoint extends Point {
public readonly weight: number = 1
}
// My mixin just needs the facilities of the base class.
const SummedSquared = (Base: Constructor<Point>) =>
class extends Base {
getSumOfSquares(): number {
return this.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.
class SummedSquaredWeightedPoint extends SummedSquared(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'.
)
}
} |
@thejohnfreeman This playground solves your follow-up question. Before: const SummedSquared = (Base: Constructor<Point>) =>
class extends Base {
getSumOfSquares(): number {
return this.x * this.x + this.y * this.x
}
}; After: const SummedSquared = <T extends Constructor<Point>>(Base: T) =>
class extends Base {
getSumOfSquares(): number {
return this.x * this.x + this.y * this.x
}
}; Pulls the types quite nicely: |
i have the same question import * as mongoose from 'mongoose';
export declare type InstanceType<T> = T & mongoose.Document;
export declare type ModelType<T, typeofT> = mongoose.Model<InstanceType<T>> & typeofT;
declare var _Model1: <T>(t?: T) => mongoose.Model<InstanceType<T>>;
export declare class Model<T, DocOmit={}> extends _Model1() {
createdAt?: Date;
updatedAt?: Date;
}
type FooModelType = ModelType<Foo, typeof Foo>;
class Foo extends Model<Foo>{
static async method() {
//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>;
// }
let rs = await this.findOne();
//and now the result is mongoose.Document
//but i want to get InstanceType<Foo>
let self = this as FooModelType;
let rs2 = await self.findOne();
//what i need is
//class Model<T, DocOmit={}> extends _Model1<T>() {}
}
} |
Search Terms
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):
Now if I want to specify the type argument for
Tagged
, that's easy enough 1 [playground]:But if I want to pass a derived class's type parameter as that argument, I cannot [playground]:
Checklist
My suggestion meets these guidelines:
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? ↩The text was updated successfully, but these errors were encountered: