Search code examples
angulartypescript

How to define a Record of Angular Components that inherit from an abstract class with an InputSignal property?


I try to define a Record like this:

// imports...

export abstract class AbstractComponent<T extends string> {
  abstract prop: InputSignal<T>;
}

export class TestComponent extends AbstractComponent<'test'> {
  prop: InputSignal<'test'> = input('test');
}

@Component({ standalone: true, template: `` })
export class App {
  component: Record<string, typeof AbstractComponent<string>> = {
    'test-1': TestComponent,
  };
}

And I get this error:

Type 'typeof TestComponent' is not assignable to type 'typeof AbstractComponent<string>'.
  Construct signature return types 'TestComponent' and 'AbstractComponent<string>' are incompatible.
    The types of 'prop[SIGNAL].transformFn' are incompatible between these types.
      Type '((value: "test") => "test") | undefined' is not assignable to type '((value: string) => string) | undefined'.
        Type '(value: "test") => "test"' is not assignable to type '(value: string) => string'.
          Types of parameters 'value' and 'value' are incompatible.
            Type 'string' is not assignable to type '"test"'.(2322)
  • In the real project, the T extends string generic is an interface and the 'test' literal type is its sub-type.
  • The Record's values have to be type-safe so that it was not possible to assign component = { 'a': null } or pass a type that doesn't extend string to the signal

How do I fix the error?


Solution

  • After a whole day of trial, error and confusion i've come up with this:

    export class App {
      component: Record<string, typeof AbstractComponent<string>> = {
        'test-1': TestComponent as typeof AbstractComponent<string>,
      };
    }
    

    I've also made a more complex demo of why I have so many restrictions and concerns about type safety. It's also available on StackBlitz.

    import { Component, InputSignal, input } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    
    interface MyType<T extends Record<string, any> = Record<string, any>> {
      value: T;
    }
    
    export abstract class AbstractComponent<T extends MyType> {
      abstract prop: InputSignal<T>;
    }
    
    type Test1Type = MyType<Test1Value>;
    
    interface Test1Value {
      verySpecificData: { a: number; b: boolean; c: null };
    }
    
    export class Test1Component extends AbstractComponent<Test1Type> {
      readonly prop = input.required<Test1Type>();
      readonly data = this.prop().value.verySpecificData;
    }
    
    type Test2Type = MyType<Test2Value>;
    
    interface Test2Value {
      differentData: { d: string; e: object };
    }
    
    export class Test2Component extends AbstractComponent<Test2Type> {
      readonly prop = input.required<Test2Type>();
      readonly data = this.prop().value.differentData;
    }
    
    @Component({ standalone: true, template: `` })
    export class App {
      components: Record<string, typeof AbstractComponent<MyType>> = {
        'test-1': Test1Component as typeof AbstractComponent,
        'test-2': Test2Component as typeof AbstractComponent,
      };
    }
    
    bootstrapApplication(App);