skip to content

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%

answer

  1. a key that is also a type
  2. contract without implementation
  3. InjectionToken for non-class values
  4. strings: untyped and collision-prone

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.

solid answer

~40 s

Pick the key by what is being injected. A **concrete class** is simplest when there is one real implementation: token and default implementation in one, but every file that injects it references that implementation. An **abstract class** is a runtime value too, so `inject(Clock)` works, and it is also the type: implementations `extend` it, it can hold abstract methods and shared helper code, and consumers depend on the contract, not on `SystemClock` or `FixedClock`. Use **`InjectionToken<T>`** when the value has no class — a string, a configuration object, a function, an array. **String tokens** such as `{ provide: 'apiUrl', … }` with `@Inject('apiUrl')` are legacy: they carry no type, any code using the same string silently shares the key, and `inject()`'s signature accepts classes, abstract classes and `InjectionToken`s (plus the special `HostAttributeToken`), never strings.

code

ts · 32 lines
ts
import { Component, Injectable, InjectionToken, inject } from '@angular/core';

// Abstract class: runtime key + type contract + shared helper.
export abstract class Clock {
  abstract now(): number;
  isPast(timestamp: number): boolean {
    return timestamp < this.now();
  }
}

@Injectable()
export class SystemClock extends Clock {
  now() { return Date.now(); }
}

@Injectable()
export class FixedClock extends Clock {
  now() { return Date.UTC(2026, 0, 1); }
}

// InjectionToken: a bare function has no class of its own.
export const NOW_FN = new InjectionToken<() => number>('NOW_FN', { factory: () => () => Date.now() });

@Component({
  selector: 'app-offer-banner',
  providers: [{ provide: Clock, useClass: SystemClock }],
  template: `@if (expired) { <p>This offer has ended.</p> }`,
})
export class OfferBanner {
  private readonly clock = inject(Clock); // typed as Clock, no generic needed
  protected readonly expired = this.clock.isPast(Date.UTC(2026, 5, 30));
}

go deeper

for a junior

Know the three token kinds inject() accepts, and that strings are legacy and untyped.

for a middle

Explain why an abstract class works as a token when an interface cannot, and when a non-class value calls for InjectionToken.

for a senior

Choose the key by how many implementations exist and what consumers should depend on, and plan the string-token migration.

for a principal

Standardise token conventions across teams: abstract-class contracts for swappable services, typed InjectionTokens for configuration, no strings.

## The four kinds of key A DI **token** is the runtime value the injector uses to find a provider. Angular's `ProviderToken<T>` type — what `inject()` accepts — is a union of three things: a class (`Type<T>`), an abstract class (`AbstractType<T>`) and an `InjectionToken<T>`. Older code also used plain strings. | Token kind | Exists at runtime | Carries a type | Can carry implementation | Typical use | |---|---|---|---|---| | Concrete class | yes | yes | yes, it *is* the implementation | a service with one real implementation | | Abstract class | yes | yes | shared helpers only; abstract members | a contract with several implementations | | `InjectionToken<T>` | yes | via `T` | only through a factory default | strings, config objects, functions, arrays | | String | yes | no | no | legacy code only | ## Concrete class: the default choice For most services the class is both the token and the implementation: `inject(OrdersApi)`. It is the least ceremony and, with `providedIn: 'root'`, tree-shakable. Its limitation is that every consumer references the concrete class, so substituting an implementation means registering another class under that same key, and the original implementation stays imported by consumers. ## Abstract class: a contract that is also a key An abstract class compiles to a real JavaScript class, so it survives to runtime and can be a token — unlike an interface. It is also a TypeScript type, so `inject(Clock)` is typed as `Clock` with no generic parameter. That combination makes it a good fit when: 1. **There are several implementations** — `SystemClock` in the app, `FixedClock` in demos or tests — and consumers should depend only on the contract. 2. **The contract has behaviour worth sharing** — an abstract `Clock` can declare `abstract now(): number` and implement a concrete helper such as `isPast(ts)` on top of it. 3. **You want the key to stay small** — the abstract class holds no heavy implementation, which is the idea behind library authors' lightweight tokens. Implementations `extend` the abstract class (or `implement` it), and a provider maps the key to one of them with `useClass` or `useExisting`. The key is still a class, so the choice is visible in the type system and in stack traces. ## InjectionToken: for values that are not classes Strings, numbers, configuration objects, functions and arrays have no class of their own. For them, `new InjectionToken<T>('description')` is the right key: a unique runtime object plus a compile-time type. An abstract class is the wrong tool for a bare function such as `() => number`; the abstract class describes an object with methods, and a function value does not satisfy that type. ## Abstract class or interface plus InjectionToken? For a swappable service you can also pair an interface with an `InjectionToken<Clock>`. The trade-off is small but real. The interface-and-token pair keeps the contract purely structural and lets an implementation satisfy several interfaces at once; it needs two declarations and carries no code. The abstract class is one declaration, can carry shared helpers, and gives `inject()` its type without a generic, but an implementation can extend only one base class. Teams usually pick one convention and apply it consistently. ## Why string tokens are discouraged String tokens predate `InjectionToken`. They still appear in older code as `{ provide: 'apiUrl', useValue: '/api' }` with a constructor parameter decorated `@Inject('apiUrl')`. Their problems: - **No type safety.** The string says nothing about the value; every consumer must annotate the type by hand, and nothing keeps those annotations consistent. - **Collisions.** Two unrelated pieces of code that both pick `'apiUrl'` share one key without knowing it — the opposite of `InjectionToken`, where identity guarantees uniqueness. - **Not accepted by `inject()`'s signature.** `inject()` is declared for `ProviderToken<T>`, which has no string member, and the string overload of `Injector.get` has been deprecated since Angular v4. - **Typos fail at runtime.** `'apiURL'` versus `'apiUrl'` compiles fine and fails as a no-provider error. Migrating is mechanical: declare `export const API_URL = new InjectionToken<string>('API_URL')`, replace the string in providers and consumers, and let the compiler find the rest. ## Choosing quickly - One implementation, a class: use the class. - Several implementations behind one contract: use an abstract class. - A value that is not a class instance: use `InjectionToken<T>`. - A string: only while migrating away from it.

  • Can you import an abstract class token with import type and still inject it?
    No. `import type` brings in only the type, which is erased, but `inject(Clock)` needs the class as a runtime value. Use a normal import for anything passed to `inject()` or used as `provide`; type-only imports are fine for annotations alone.
  • How would you migrate a codebase from a 'apiUrl' string token?
    Declare one exported `InjectionToken<string>`, replace the string in every provider and in every `@Inject('apiUrl')` or `injector.get('apiUrl')` call, and switch consumers to `inject(API_URL)`. The compiler then types every use, and any stray string reference shows up as a no-provider error in tests.

saying these in an interview costs you the question

  • An abstract class is erased at compile time like an interface
  • String tokens are the recommended way to inject primitive values
  • InjectionToken is required even when the dependency is a class
  • An abstract class token needs a generic type parameter for inject()
  • Two libraries using the string token 'apiUrl' get separate values