skip to content

Token Design

InjectionToken<T> names a dependency that has no class, optionally with its own factory and providedIn. Interviewers ask when an abstract class makes a better token and how tokens tree-shake.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Angular, why can't a TypeScript interface serve as a DI token, and how does an InjectionToken solve that for an API_BASE_URL string?

level: juniorimportance: must knowfreq 62%

answer

  1. types vanish after compilation
  2. a token must exist at runtime
  3. identity by object reference
  4. generic parameter types inject()
  5. description is for debugging only

basics

~20 s

TypeScript interfaces are erased at compile time, so there is nothing at runtime for the injector to use as a key. An InjectionToken<T> is a real object that serves as that key, and its type parameter makes inject() return T.

solid answer

~40 s

Angular's injector looks providers up by a runtime value, and interfaces and type aliases do not survive compilation, so `{ provide: ApiSettings, … }` with an interface cannot even compile as a value. Primitive values have the same problem: a string such as a base URL has no class to act as its key. `new InjectionToken<string>('API_BASE_URL')` creates a unique object to use instead. You register it once — `{ provide: API_BASE_URL, useValue: 'https://api.example.test' }` — and `inject(API_BASE_URL)` returns a value typed as `string`, because `inject()` reads `T` from the token. The string argument is only a description for error messages; the injector matches tokens by object identity, so the token must be exported once and imported everywhere, not re-created in each file.

code

ts · 28 lines
ts
import { ApplicationConfig, Injectable, InjectionToken, inject } from '@angular/core';
import { HttpClient, provideHttpClient } from '@angular/common/http';

// tokens.ts - declared once, imported everywhere
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');

export interface Order {
  id: string;
  totalCents: number;
}

@Injectable({ providedIn: 'root' })
export class OrdersApi {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = inject(API_BASE_URL); // typed as string

  list() {
    return this.http.get<Order[]>(`${this.baseUrl}/orders`);
  }
}

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    { provide: API_BASE_URL, useValue: 'https://api.example.test' },
  ],
};

go deeper

for a junior

Recall that interfaces disappear after compilation, so non-class dependencies need an InjectionToken, and that inject() returns the token's T.

for a middle

Explain identity-based matching, what the description is for, and why a token must be declared once and imported by both sides.

for a senior

Diagnose the duplicated-token failure, including duplicates coming from two copies of a library, and decide when a class key is clearer than a token.

for a principal

Set conventions for where shared tokens live and who owns them, so configuration keys do not multiply or drift across teams and libraries.

## Why a token has to be a runtime value Angular's **injector** is, at its core, a map from **tokens** to providers. When code calls `inject(SomeToken)`, the injector looks up `SomeToken` in its records, walking up the injector tree until it finds a provider. That lookup is done in JavaScript, at runtime, so the token has to be a JavaScript value. A class works as a token because a class *is* a runtime value — a constructor function. A TypeScript `interface` or `type` alias does not: it exists only for the type checker and is **erased** during compilation. Writing `inject(ApiSettings)` where `ApiSettings` is an interface fails to compile, because there is no value named `ApiSettings` to pass. The same gap appears for anything that is not a class instance: - a primitive such as a base URL string or a retry count, - a plain configuration object described by an interface, - a function, such as a `now(): number` clock, - an array, such as a list of contributions. ## What `InjectionToken` provides `InjectionToken<T>` from `@angular/core` fills that gap. Each `new InjectionToken<T>(description)` call creates a distinct object that can be used as a key: ```ts export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL'); ``` It gives you three things: 1. **A unique runtime key.** The injector compares tokens by **object reference**. Two tokens created with the same description are still two different tokens. 2. **Type information.** The generic parameter `T` is carried in the token's type, and `inject()` is declared to return `T` for an `InjectionToken<T>`. `inject(API_BASE_URL)` is therefore typed as `string` with no cast. 3. **A readable name in errors.** The description is used when Angular prints the token, for example in a no-provider error, so `API_BASE_URL` shows up in the message instead of an anonymous object. ## Providing and consuming the token The token is only the key; a provider supplies the value. For a base URL that differs per deployment, a `useValue` provider at application level is typical, and every service that needs it injects the token: ```ts { provide: API_BASE_URL, useValue: 'https://api.example.test' } private readonly baseUrl = inject(API_BASE_URL); // string ``` Consumers do not know or care where the URL came from — a build-time environment file, a value read before bootstrap, or a test. ## The identity trap | Situation | Result | |---|---| | One exported `API_BASE_URL`, imported by provider and consumers | Works | | Provider and consumer each call `new InjectionToken('API_BASE_URL')` | Different tokens: the consumer gets a no-provider error (NG0201) | | Same description reused by two libraries for different tokens | No clash: identity, not the description, decides | Because identity is what matters, define each token **once**, in a small module that both the providing code and the consuming code import. Re-declaring a token in a second file "with the same name" is a classic bug: everything compiles, and the lookup fails at runtime. ## Tokens in libraries and shared packages The identity rule has a second, less obvious consequence. A library that exports a token must be loaded **once**. If an application ends up with two copies of the same library — two versions resolved side by side, or a package bundled into another package — each copy evaluates its own `new InjectionToken(...)` call and produces its own token object. The application may provide the token from one copy while a component from the other copy injects it, and the lookup fails even though every file looks correct. When a no-provider error names a token you can see being provided, check for duplicate copies of the package before anything else. Libraries should also export their tokens from the public entry point, so consumers never need to re-declare one. ## When a class is still the better key If the dependency is naturally a class with behaviour, use the class (or an abstract class) as the token and skip `InjectionToken` entirely. `InjectionToken` is for values that have no class of their own. Choosing between class, abstract-class and `InjectionToken` keys is a design decision worth making deliberately rather than by habit. ## Summary - Interfaces and type aliases cannot be tokens because they do not exist at runtime. - `InjectionToken<T>` is a runtime key plus a compile-time type. - The description string helps debugging but plays no part in matching. - Export each token once and import it everywhere.

  • Your service throws a no-provider error for InjectionToken API_BASE_URL, yet app.config.ts clearly provides API_BASE_URL. What do you check first?
    Whether both files import the same token object. A second `new InjectionToken('API_BASE_URL')` in another file — often a copy-pasted declaration or a duplicated library build — is a different key with the same description, so the provider and the consumer never meet. Consolidate to one exported declaration.
  • Does the type parameter of InjectionToken check the value you provide at runtime?
    No. `T` exists only for the type checker. `inject()` is typed to return `T`, and a typed provider helper can constrain what you pass, but a plain `{ provide: API_BASE_URL, useValue: 42 }` object literal is not checked against `T`, and Angular does not validate the value at runtime.

saying these in an interview costs you the question

  • An interface can be used as a token if it is exported
  • Two InjectionTokens with the same description are the same token
  • The description string is the key the injector looks up
  • Angular checks at runtime that the provided value matches the token's T
  • InjectionToken is only needed for NgModule-based apps
open as a page

In Angular, when is an abstract class a better DI token than an InjectionToken or a concrete class, and why are string tokens discouraged?

level: middleimportance: should knowfreq 40%

basics

~20 s

An abstract class exists at runtime, so it can be a token, and it doubles as the type contract implementations extend, without shipping an implementation. InjectionToken suits non-class values. String tokens lose type safety, can collide, and inject() does not accept them.

open as a page

In Angular, what does giving an InjectionToken a factory do, and how does an explicit provider for the same token interact with it?

level: middleimportance: should knowfreq 44%

basics

~20 s

A factory makes an InjectionToken self-providing: it behaves as if registered in the root injector, so it never needs a providers entry and is tree-shakable. Any explicit provider found first on the lookup path overrides that default.

open as a page

In Angular, how would you design a typed configuration-object token for an API client so that defaults apply and deployments override only what they need?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Declare one InjectionToken for a readonly config interface, give it a factory that returns complete defaults, and expose a small provider function that merges a partial override with those defaults into a full object, so consumers always inject a complete, typed configuration.

open as a page

In an Angular component library, what is the lightweight injection token pattern, and why does it let an unused component be tree-shaken?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

A library component that queries or injects another component by class keeps that class in the bundle even when unused. The pattern swaps the class for a small abstract token the optional component provides via useExisting, so only the tiny token is retained.

open as a page