#How Can I Achieve Type-Safe Dynamic Component Loading in Angular Using InjectionToken?

3 messages · Page 1 of 1 (latest)

gray umbra
#

In my application, I want to load a profile component using an InjectionToken by passing it into the providers. When I invoke a function, I want to load this component and pass input properties to it.

This approach works, but not with TypeScript because everything is typed as any.

My question is, can this be done in a type-safe manner? Currently, the profile component is typed as any.

The issue is that the profile component is located in a different path, and I don't want to directly reference it in the service file. Additionally, I don't want to share interfaces between the components by using implements for some interface. Instead, I want to make the InjectionToken itself type-safe.

import {
  Component,
  Injectable,
  InjectionToken,
  Input,
  inject,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';

const Config = new InjectionToken<{ profile: any }>('config');

@Injectable({ providedIn: 'root' })
export class Service {
  config = inject(Config);

  async doWork() {
    const profile = await this.config.profile();
    console.log({ profile });

    profile.firstname = 'john';
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  providers: [
    Service,
    {
      provide: Config,
      useValue: {
        profile: () =>
          import('./profile.component').then((c) => c.ProfileComponent),
      },
    },
  ],
  template: `
    <h1>Hello from {{ name }}!</h1>
    <a target="_blank" href="https://angular.dev/overview">
      Learn more about Angular
    </a>

    <button (click)="doWork()">do work</button>
  `,
})
export class App {
  name = 'Angular';
  service = inject(Service);

  doWork() {
    this.service.doWork();
  }
}

bootstrapApplication(App);

Here is the example I created: https://stackblitz.com/edit/stackblitz-starters-vrlhsg

rigid coral
#

You should tell us which concrete problem you're trying to solve, because this looks weird. But since you expect config.profile to be a function returning a promise of ProfileComponent, then use that as a type:

new InjectionToken<{ profile: () => Promise<ProfileComponent> }>(
  'config'
)
gray umbra
#

However, this solution requires the injection token file to know about ProfileComponent. Instead, it would be better if it was something like:

profile: () => Promise<Type<{ firstname: string; lastname: string }>>

Then, in the component providers, TypeScript would check if the component has those inputs:

providers: [{ provide: config, useValue: () => import(...).ProfileComponent }]

And it would throw an error if the component doesn't have those inputs:

providers: [{ provide: config, useValue: () => import(...).OtherComponent }]

The specific type of the component doesn't matter as long as it has those inputs. Additionally, I don't want to use implements to enforce the same interface.