Search code examples
angulartypescript

Angular: Having an array of extended objects - how to correctly use types?


I have a base class with multiple extensions looking like this:

export interface ProjectOnePagerDTO {
  Widgets: IOnePagerWidget[];
  Id: string;
}

export interface IOnePagerWidget extends GridsterItem {
  Type: OnePagerWidgetType;
}

export interface OnePagerWidgetTypePicture extends IOnePagerWidget {
  ProjectLogo: string;
}

export interface OnePagerWidgetTypeProjectDescription extends IOnePagerWidget {
  Text: string;
}

Each extension, e.g. OnePagerWidgetTypePicture, has its own component. In the template I loop through Widgets property of ProjectOnePagerDTO and pass each widget to its component. The component for OnePagerWidgetTypePicture looks like this

export class OpwProjectPictureComponent {
  @Input() widget: OnePagerWidgetTypePicture;
}

Now I am getting following error: Property 'ProjectLogo' is missing in type 'IOnePagerWidget' but required in type 'OnePagerWidgetTypePicture'.

What am I missing? Do I have to change the @Input type and cast it right inside of the component?


Solution

  • Method 1:

    Try using Generics so that it accepts multiple types.

    @Component({
      selector: 'app-child',
      template: ``,
    })
    export class Child<T> {
      @Input() widget!: T;
    }
    

    Method 2:

    Try using a function that determines the type based on the properties unique to that type.

    typeIt(widget: CompositeWidgetType) {
      if ((widget as OnePagerWidgetTypePicture).ProjectLogo) {
        return widget as OnePagerWidgetTypePicture;
      } else if ((widget as OnePagerWidgetTypeProjectDescription).Type) {
        return widget as OnePagerWidgetTypeProjectDescription;
      } else {
        return widget as IOnePagerWidget;
      }
    }
    

    Full Code:

    import { Component, Input } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    
    export class GridsterItem {}
    
    export class OnePagerWidgetType {}
    
    export interface ProjectOnePagerDTO {
      Widgets: IOnePagerWidget[];
      Id: string;
    }
    
    export interface IOnePagerWidget extends GridsterItem {
      Type: OnePagerWidgetType;
    }
    
    export interface OnePagerWidgetTypePicture extends IOnePagerWidget {
      ProjectLogo: string;
    }
    
    export interface OnePagerWidgetTypeProjectDescription extends IOnePagerWidget {
      Text: string;
    }
    
      @Component({
        selector: 'app-child',
        template: ``,
      })
      export class Child<OnePagerWidgetTypePicture> {
        @Input() widget!: OnePagerWidgetTypePicture;
      }
    
    export type CompositeWidgetType =
      | IOnePagerWidget
      | OnePagerWidgetTypePicture
      | OnePagerWidgetTypeProjectDescription;
    
    @Component({
      selector: 'app-root',
      imports: [Child],
      template: `
        @for (widget of Widgets;track $index) {
          @let typedWidget = typeIt(widget);
          <app-child [widget]="typedWidget"/>
        }
      `,
    })
    export class App {
      Widgets!: Array<CompositeWidgetType>;
    
      typeIt(widget: CompositeWidgetType) {
        if ((widget as OnePagerWidgetTypePicture).ProjectLogo) {
          return widget as OnePagerWidgetTypePicture;
        } else if ((widget as OnePagerWidgetTypeProjectDescription).Type) {
          return widget as OnePagerWidgetTypeProjectDescription;
        } else {
          return widget as IOnePagerWidget;
        }
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo

    Method 3:

    If the parent component contains either of the three values, then shouldn't the array specify the same?

    Here we can specify a type named CompositeWidgetType and assign it to the array.

    export type CompositeWidgetType = IOnePagerWidget | OnePagerWidgetTypePicture | OnePagerWidgetTypeProjectDescription;
    
    export interface ProjectOnePagerDTO {
      Widgets: Array<CompositeWidgetType>;
      Id: string;
    } 
    

    Now when you pass in to the child component, it will take the respective type, since the type can be any of the three types.