Issue
I'd like to define some mixin functionality (i.e. the standard TypeScript mixins pattern) based on generic data structures that have known interfaces but will be fully finalized later.
Many of the base classes I'd like to apply them to also have some generic aspects.
It seems perfectly possible to apply non-generic mixins to generic classes, for example the below where WithHouse
is fully specified up-front and Pet
takes a TFavoriteToy
argument:
class Dog<TFavoriteToy extends object> extends WithHouse(Pet)<TFavoriteToy> {}
...But in the case where the mixin needs its own type parameterization, I can't find any way to apply it except in cases where I'm ready to fully specify all type arguments already.
Consider the following brief example:
/**
* Abstract base class with partial implementation
*/
export class BaseThing<TData extends object> {
dataSource: TData;
constructor(data: TData) {
this.dataSource = data;
}
}
type Constructor<T> = (new (...args: any[]) => T);
/**
* A mixin with generic-ified functionality
*/
export function WithCustomer<
T extends Constructor<{}>,
TCustomer extends {name: string}
>(SuperClass: T) {
return class extends SuperClass {
_cnames: string[];
constructor(...args: any[]) {
super(...args);
this._cnames = [];
}
logCustomer(customer: TCustomer): TCustomer {
this._cnames.push(customer.name);
return customer;
}
};
}
// Imagine concrete instances of TData and TCustomer:
type TActualData = {count: number};
type TActualCust = {name: string; phone: string};
/**
* It seems okay to apply the mixin *after* fulfilling all type args in both
* base and mixin:
*/
export class WorkingThing extends WithCustomer<
Constructor<BaseThing<TActualData>>,
TActualCust
>(BaseThing) {
constructor(data: TActualData) {
super(data);
this.logCustomer({name: 'Joe', phone: '123'});
}
}
/**
* The problem class: Applying the mixin without resolving the underlying
* generics... Error: "No base constructor has the specified number of type
* arguments. ts(2508)"
*/
export class BrokenThing<TData, TCustomer>
extends WithCustomer(BaseThing)<TData, TCustomer>
{
// (Aim to implement extra features that are still generic to some extent)
}
I've tried various locations for the type annotations in the extends
expression, but haven't found a way to get it working. Even in the seemingly-simpler case where we'd be okay to solidify BaseThing<TActualData>
as the base class first, before applying the mixin, I can't see a solution?
So far it seems like it might just not be possible to implement type-generic functionality in a mixin... But I can't see why that should be from a prototype chain perspective, and would love if there's just some syntactic tweak I'm missing!
Solution
The best solution I found so far for this (from this other SO answer) works only in cases where we're able to resolve the generics introduced by the mixin, at the point where the mixin is applied:
export function buildWithCustomer<TCustomer extends {name: string}>() {
return function WithCustomer<T extends Constructor<{}>>(SuperClass: T) {
return class extends SuperClass {
_cnames: string[];
constructor(...args: any[]) {
super(...args);
this._cnames = [];
}
logCustomer(customer: TCustomer): TCustomer {
this._cnames.push(customer.name);
return customer;
}
};
};
}
// TActualCust must be concrete (cannot be an argument to WorkaroundThing)
export class WorkaroundThing<TData extends { count: 2 }>
extends buildWithCustomer<TActualCust>()(BaseThing)<TData> {
constructor(data: TData) {
super(data);
// Can access defined properties from both base class and mixin:
for (let i = 0; i < this.dataSource.count; ++i) {
this.logCustomer({name: 'Joe', phone: '123'});
}
}
}
Interestingly it seems like the opposite pattern (to resolve the base class generics but keep the new ones at the point of mixin application) does not work:
export function buildWithCustomer2<T extends Constructor<{}>>(SuperClass: T) {
return function WithCustomer<TCustomer extends {name: string}>() {
// (Implementation as above)
};
}
// No base constructor has the specified number of type arguments.ts(2508)
// (expected 1, got 0)
export class WorkaroundThing2<TCustomer extends TActualCust>
extends buildWithCustomer<TActualData>(BaseThing)()<TCustomer> {
// ...
}
As far as I can tell it's probably not possible to achieve the full general case of applying a generic mixin functionality to a generic base class, without resolving either set of type arguments yet - but would love to be wrong.
Answered By - dingus
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.