Reputation: 1039
When using a mixin that builds up on a generic class, I need to set it's value to unknown, causing the implementing class having a generic parameter of type unknown | concrete
.
I've build an example here using Angular, but it's completely Typescript related: https://stackblitz.com/edit/angular-svw6ke?file=src%2Fapp%2Fapp.component.ts
Is there any chance to redesign this mixin (using Typescript 4.4) so the type won't get malformed?
Upvotes: 0
Views: 485
Reputation: 27264
You need to carry the generic parameter through the mixin function. Titian's answer here gives a good explanation of how this works. For your problem, it would look something like
export function genericMixin1<
ModelType,
ComponentType extends AbstractConstructor<ComponentBase<ModelType>>
>(Base: T) {
abstract class GenericMixin extends Base {
//Logic here
}
return GenericMixin;
}
It helps to know going in that what he calls the "two call approach" stems from a workaround for a common problem with TypeScript generics -- you can't partially infer generic type parameters when calling a function, but you can sort of "divide" the generic parameters between the one you're calling and a second inner function. For example:
export function genericMixin2<T>() {
return function<
ComponentType extends AbstractConstructor<ComponentBase<T>>
>(Base: ComponentType) {
abstract class GenericMixin extends Base {
//Logic here
}
return GenericMixin;
};
}
(ETA: Fixed, Playground example here.)
Now instead of writing const StringMixin = genericMixin1<string, ComponentBase<string>>(ComponentBase);
you can write const StringMixin = (genericMixin2<string>()(ComponentBase))
. This is only a slight improvement with one generic parameter, but becomes absolutely necessary when you have many generic params. Otherwise, you wind up with const ManyParamsMixin = genericMixin<string, number, boolean, BaseThing<string,number,boolean>>(BaseThing)
... it gets ugly fast.
Upvotes: 1
Reputation: 371
You can forgo the extends
clause in your mixin function and just use a generic T
to represent your BehaviorSubject
type:
export abstract class ComponentBase<T> {
model$ = new BehaviorSubject<T>(null);
}
export function genericMixin<T>(Base: AbstractConstructor<ComponentBase<T>>) {
abstract class GenericMixin extends Base {
//Logic here
}
return GenericMixin;
}
This works without having to do any casting. Your AppComponent
class will also extend a little differently:
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent extends genericMixin<Model>(ComponentBase) {
name = 'Angular';
method(): void {
this.model$.subscribe((v) => console.log(v.value));
}
}
But your model$
property should still infer correctly. Let me know if that works!
You can extract the type of your AbstractConstructor
base class argument and explicitly annotate your return type from the mixin function, like so:
// An interface that represents what you're mixing in
interface WithModel<T> {
model$: BehaviorSubject<T>;
}
// Extracting the type of your `AbstractConstructor` parameter
type ExtractModelType<T extends AbstractConstructor<ComponentBase<any>>> =
T extends AbstractConstructor<ComponentBase<infer U>> ? U : never;
// Then in your mixin function
export function genericMixin<
T extends AbstractConstructor<ComponentBase<unknown>>
>(Base: T): T & WithModel<ExtractModelType<T>> {
abstract class GenericMixin extends Base {
// Logic here
}
return GenericMixin as T & WithModel<ExtractModelType<T>>;
}
Now accessing the model$
parameter from derived classes should yield the correct type.
class Model {
value: string;
}
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent extends genericMixin(ComponentBase)<Model> {
name = 'Angular';
method(): void {
// `model$` is correctly inferred as a `Model` here.
this.model$.subscribe((v) => console.log(v.value));
}
}
Upvotes: 2