0

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?

2 Answers 2

2

Update: simpler solution

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!

First solution

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));
  }
}
Sign up to request clarification or add additional context in comments.

11 Comments

The first solution works perfect, thank you! The simplified one doesn't seam to work. Can you maybe modify the Stackblitz regardingly?
Oh no. The first solution stopped working too, as soon as you put any logic in it. Replace //Logic here even with s: string; and the build fails. Check out here. stackblitz.com/edit/….
Hmm.. you're right about the first solution failing as soon as you add additional properties. I couldn't find a way around that without extensive type casting. Check out a working implementation of the simpler solution here.
Too bad this leads to two new issues (marked as #1 & #2): stackblitz.com/edit/… #1: I cannot prepare generic mixin combinations anymore since the type need to be set the time it is declared. #2: The type-information is lost and mixins cannot be combined any more. But: its getting closer!
Check out this example. Seems to be working for multiple mixins. The implementations of your genericMixin2 function and ComponentDefault have changed a bit. Let me know if that works!
|
1

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.

11 Comments

Something seams to be wrong with your syntax here. I can't use the code export function genericMixin2<T>() { return function<ComponentBase<T>>(Base: T) { abstract class GenericMixin extends Base { //Logic here } return GenericMixin; }; } TS doesn't seam to like "return function < ..."
Thanks, I made an error transcribing the generic parameter definition for the nested function. It should work now.
Thank you very much, but still something seams to be wrong / I don't get it. Error occurs as soon as I use my mixin: typescriptlang.org/play?declaration=false#code/…
Ah, I see! The idea is to have a base function creating my class instead of a base class! Sure that's the solution!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.