Skip to content
Angular ng services 4 min read

Injection Tokens

Angular’s dependency injection uses a token as the key for every lookup, and most of the time that token is a class. But interfaces, primitives, and plain configuration objects have no class to act as a key — TypeScript interfaces vanish at runtime, and you can’t ask the injector for a string. The InjectionToken solves this: it creates a unique, type-safe key for any value, so configuration and constants flow through DI just like services do.

Why classes aren’t enough

A class doubles as both a token and a runtime value, which is why injecting a service “just works”. Non-class values break this assumption in two ways:

  • Interfaces and types are erased during compilation, so there is nothing left at runtime to use as a key.
  • Primitives and objects (a base URL, a feature flag, a config bag) aren’t unique — two different strings would collide on the same key.

InjectionToken gives you a single object that is guaranteed unique and carries a generic type parameter for compile-time safety.

Creating a token

You create a token once and export it. The generic type describes what the token resolves to, and the string argument is a human-readable description used only in error messages.

import { InjectionToken } from '@angular/core';

export interface AppConfig {
  apiUrl: string;
  retryCount: number;
  featureFlags: Record<string, boolean>;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export const API_URL = new InjectionToken<string>('api.url');

The token’s type travels with it everywhere — when you inject APP_CONFIG, Angular knows it’s an AppConfig, and when you provide it, TypeScript rejects a mismatched shape.

Providing a value

Because there’s no class to instantiate, you wire a token with an explicit provider recipe — most commonly useValue for static data or useFactory for computed values.

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { APP_CONFIG, API_URL } from './app/tokens';

bootstrapApplication(AppComponent, {
  providers: [
    { provide: API_URL, useValue: 'https://api.example.com' },
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: 'https://api.example.com',
        retryCount: 3,
        featureFlags: { darkMode: true },
      },
    },
  ],
});

Injecting a token

Inject a token with the same inject() function you use for services. The return type is inferred from the token’s generic, so autocomplete and type-checking work end to end.

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { APP_CONFIG, API_URL } from './tokens';

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private config = inject(APP_CONFIG); // typed as AppConfig
  private apiUrl = inject(API_URL);    // typed as string

  getUsers() {
    console.log(`Calling ${this.apiUrl} (retries: ${this.config.retryCount})`);
    return this.http.get(`${this.apiUrl}/users`);
  }
}

Output:

Calling https://api.example.com (retries: 3)

Tree-shakable tokens with a default factory

Just as services use providedIn: 'root', a token can carry its own default factory. If nobody provides the token explicitly, Angular runs the factory — and if nobody injects it, the bundler drops it entirely.

export const RETRY_COUNT = new InjectionToken<number>('retry.count', {
  providedIn: 'root',
  factory: () => 3,
});

A factory can even inject other dependencies, letting one token derive its value from another:

export const HTTP_TIMEOUT = new InjectionToken<number>('http.timeout', {
  providedIn: 'root',
  factory: () => inject(APP_CONFIG).retryCount * 1000,
});

Tip: Prefer a token with a default factory over a separately registered useValue provider when there’s a sensible fallback — it keeps the token self-contained and tree-shakable, and you can still override it in tests or per feature.

Multi-value tokens

Setting multi: true makes a token collect all contributions into an array instead of overwriting. This is how Angular’s own HTTP_INTERCEPTORS works, and it’s perfect for plugin-style registration.

export const VALIDATORS = new InjectionToken<((v: string) => boolean)[]>('validators');

const providers = [
  { provide: VALIDATORS, useValue: (v: string) => v.length > 0, multi: true },
  { provide: VALIDATORS, useValue: (v: string) => v.includes('@'), multi: true },
];

// inject(VALIDATORS) resolves to an array of both functions

Recipe reference

RecipeUse it when
useValueThe value is a known constant or pre-built object.
useFactoryThe value must be computed, possibly from other injected deps.
factory (on the token)You want a tree-shakable default with no separate provider.
multi: trueMany providers should contribute entries to one array.

Warning: A token is identified by object reference, not by its description string. Always export and import the same InjectionToken instance — re-creating an identically-named token in another file produces a different key and a NullInjectorError.

Best practices

  • Reach for InjectionToken for every non-class dependency: config objects, base URLs, feature flags, and function values.
  • Always pass the generic type parameter so injection sites stay fully typed.
  • Give tokens a descriptive string (e.g. 'app.config') to make NullInjectorError messages readable.
  • Use a default factory with providedIn: 'root' when a sensible fallback exists, keeping the token tree-shakable.
  • Use multi: true for extensible, plugin-style lists rather than injecting and merging arrays by hand.
  • Define tokens in a dedicated tokens.ts and import that single instance everywhere to avoid duplicate-token bugs.
Last updated June 14, 2026
Was this helpful?