Declare a component with generic type

AngularTypescriptAngular ComponentsTypescript Generics

Angular Problem Overview


Is it possible to declare a component with a generic type in Angular 4?

The following code causes build errors:

export class MyGenericComponent<T> implements OnInit {
    @Input()  data: BehaviorSubject<T[]>;

    //...
}

The error when executing ng serve is:

ERROR in C:/.../my-generic.module.ts (5,10): Module '"C:/.../my-generic.component"' has no exported member 'MyGenericComponent'.

Example:

The following example is an attempt to implement a generic data table where @Input() data changes from one component 'calling this component' to another. The question is could BehaviorSubject<any[]> be changed to BehaviorSubject<T[]> where T would be the generic type passed to the component?

@Component({
  selector: 'my-data-list',
  templateUrl: './data-list.component.html',
  styleUrls: ['./data-list.component.css']
})
export class DataListComponent implements OnInit {
  @Input()  data: BehaviorSubject<any[]>;
  @Output() onLoaded = new EventEmitter<boolean>();

  private tableDataBase : TableDataBase = new TableDataBase();
  private dataSource : TableDataSource | null;

  constructor() { }

  ngOnInit() {
    this.tableDataBase.dataChange = this.data;
    this.dataSource = new TableDataSource(this.tableDataBase);
    this.onLoaded.emit(true);
  }
}

class TableDataBase {
  dataChange: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]);

  get data(): any[] {
    return this.dataChange.value;
  }
}

class TableDataSource extends DataSource<any> {

  constructor(private tableDataBase: TableDataBase) {
    super();
  }

  connect(): Observable<any[]> {
    return Observable.of(this.tableDataBase.data);
  }

  disconnect() {}
}

Angular Solutions


Solution 1 - Angular

You can also access the Type parameter through the ViewChild like this:

export class Bazz {
  name: string;

  constructor(name: string) {
    this.name = name;   
  }
}

@Component({
  selector: 'app-foo',
  template: `<div>{{bazz?.name}}</div>`,
  exportAs: 'appFoo'
})
export class FooComponent<T> {
  constructor() {}
  private _bazz: T;

  set bazz(b: T) {
    this._bazz = b;
  }

  get bazz(): T {
   return this._bazz;
  }
}

@Component({
  selector: 'app-bar',
  template: `<app-foo #appFoo></app-foo>`,
  styleUrls: ['./foo.component.scss'],
})
export class BarComponent<T> implements OnInit {
  @ViewChild('appFoo') appFoo: FooComponent<Bazz>;
  
  constructor() {}
  
  ngOnInit() {
    this.appFoo.bazz = new Bazz('bizzle');
    console.log(this.appFoo.bazz);
  }
}

Solution 2 - Angular

You can declare it, but cannot use it directly. You can do something like this:

export abstract class Form<T> implements OnInit, OnChanges {
  someMethod() { throw 'Dont use directly' }
  otherMethod() { return 'Works!'; }
  // Note that below will cause compilation error
  //   TypeError: Object prototype may only be an Object or null: undefined
  // You cannot use protected in this usecase
  protected anotherMethod() { }
}

@Component({})
export class ModelOneForm extends Form<ModelOne> {
  someMethod() { return this.otherMethod(); }
}

Solution 3 - Angular

You can consider this way. Create an interface for the data such as following:

interface ListItem {
  info: string;
  ...
}

Transform the data that you want to list to comply with the interface and thus can be interpreted by the ListDataComponent. Your ListDataComponent can then list the data according to the properties in the interface.

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'data-list',
  templateUrl: './data-list.component.html',
  styleUrls: ['./data-list.component.scss']
})
export class DataListComponent implements OnInit {
    @Input() public items: ListItem[];

    constructor() {
    }

    ngOnInit() {
    }
}

Solution 4 - Angular

I did something similar to Jun711. I created an interface, and then my component uses that interface. Then I just extend the interface for the other classes. I just pass in an array of a type that extends the interface.

export interface INameable {
	name: string;
}

export interface IPerson extends INameable { title: string; }
export interface IManager extends INameable { Employees: IPerson[]; }

@Component({
	selector: 'nameable',
	templateUrl: './nameable.component.html',
	styleUrls: ['./nameable.component.scss'],
})
export class NameableComponent implements OnInit {

	@Input() names: INameable[] = [];
	@Output() selectedNameChanged = new EventEmitter<INameable>();

	constructor() {}
	ngOnInit() {}
}

then usage is pretty simple:

<nameable [names]="PersonList" (selectedNameChanged)="personChangedHandler($event)"></nameable>
<nameable [names]="ManagerList" (selectedNameChanged)="mangagerChangedHandler($event)"></nameable>

The downside is that the containing component has to determine the full type, but the upside is my components become more reusable as I follow Liskov & ISP.

Solution 5 - Angular

I would recommend creating a parent list component with multiple child components for each type of data displayed, then use [ngSwitch] and *ngSwitchCase to determine what to display.

@Component({
  selector: 'app-list',
  template: `
    <ng-container *ngFor="let item in list$ | async" [ngSwitch]="item.type">
      <app-list-item-one [item]="item" *ngSwitchCase="listItemType.One"></app-list-item-one>
      <app-list-item-two [item]="item" *ngSwitchCase="listItemType.Two"></app-list-item-two>
    </ng-container>
  `
})
export class ListComponent extends OnInit {
  list$: Observable<ListItem[]>

  constructor(
    private listApi: ListApiService
  ) { }

  ngOnInit() {
    this.list$ = this.listApi.getList(...)
  }
}

@Component({
  selector: 'app-list-item-one',
  template: `
    {{ item.aProperty }}
  `
})
export class ListItemOneComponent {
  @Input() item: ListItemOne
}

@Component({
  selector: 'app-list-item-two',
  template: `
    {{ item.bProperty }}
  `
})
export class ListItemTwoComponent {
  @Input() item: ListItemTwo
}

export class ListItem {
  id: string
}

export class ListItemOne {
  aProperty: string
}

export class ListItemTwo {
  bProperty: string
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionStriderView Question on Stackoverflow
Solution 1 - AngularMatt RingerView Answer on Stackoverflow
Solution 2 - AngularSasxaView Answer on Stackoverflow
Solution 3 - AngularJun711View Answer on Stackoverflow
Solution 4 - AngularScott IView Answer on Stackoverflow
Solution 5 - AngularTrevorView Answer on Stackoverflow