In Angular, when is an abstract class a better DI token than an InjectionToken or a concrete class, and why are string tokens discouraged?
answer
- a key that is also a type
- contract without implementation
- InjectionToken for non-class values
- strings: untyped and collision-prone
basics
~20 sAn 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 sPick 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 linesimport { 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
Know the three token kinds inject() accepts, and that strings are legacy and untyped.
Explain why an abstract class works as a token when an interface cannot, and when a non-class value calls for InjectionToken.
Choose the key by how many implementations exist and what consumers should depend on, and plan the string-token migration.
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