Lesson 14 of 30
inject() gets you a dependency without a constructor parameter — which is exactly what makes functional guards, interceptors, and base classes possible.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Constructor injection — constructor(private http: HttpClient) {} — has always worked by declaring a parameter and letting Angular fill it in when it builds the class. It's still completely valid, current Angular, and you'll see it constantly in real codebases, including new ones.
But it has one hard requirement: you need a constructor, which means you need a class. That's a real limitation for anything that isn't naturally a class — a route guard is conceptually just "a function that returns true or false," an HTTP interceptor is conceptually just "a function that takes a request and returns a response," and forcing both into classes purely to get access to constructor injection has always been overhead the abstraction didn't otherwise need.
inject() removes that requirement. It's a function you call to retrieve a dependency without a constructor parameter — const http = inject(HttpClient) — which means it works anywhere Angular considers you to be inside an "injection context": a constructor (yes, it works there too), a field initializer, or a plain function that Angular invokes at a moment when injection is valid, like a functional guard or a functional interceptor.
import { inject } from '@angular/core';
import { Router, type CanActivateFn } from '@angular/router';
import { HttpInterceptorFn } from '@angular/common/http';
import { AuthService } from './auth.service';
// a route guard is just a function — no class required
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) return true;
router.navigate(['/login'], { queryParams: { redirectTo: state.url } });
return false;
};
// same story for an interceptor
export const authTokenInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.token();
if (!token) return next(req);
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
// registered as plain functions, not classes, when the app bootstraps
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authTokenInterceptor])),
],
});
Neither authGuard nor authTokenInterceptor is a class. There's no constructor, no implements CanActivate, nothing to instantiate — Angular calls each one as a plain function at the right moment (about to activate a route, about to send a request) and both happen to be running inside a valid injection context at that moment, so inject() works exactly as it would inside a component's constructor.
inject() is just as useful inside a class, too — it's common in modern components to see private http = inject(HttpClient); as a field initializer instead of a constructor parameter, which reads the same but doesn't require threading the dependency through a constructor signature at all, which matters more than it sounds like once a base class wants to inject something its subclasses shouldn't have to also declare.
inject() is the one that unlocks patterns constructor injection structurally can't (functions, base classes without forcing every subclass to repeat the same constructor), not a wholesale replacement for the constructor form.inject() outside a class needs the call to happen at the right time, not just the right place. It's not enough for authGuard to look like it's "in scope" for injection — Angular has to actually be the one calling that function during route activation for the injection context to be active. Wrapping the same logic and calling it manually from elsewhere breaks it.inject() in a field initializer runs before the constructor body. private http = inject(HttpClient); is resolved as the class instance is being built, which is almost always fine, but it means you can't rely on something set in the constructor body being available yet if a field initializer's inject() call somehow depended on it.inject() reading from an active injection context that only exists for the duration of a specific call is the same shape of rule as React's "only call hooks during render" — both work by relying on an ambient, framework-maintained execution context that's valid during one specific moment and gone immediately after, rather than an explicit value passed as an argument.