<link rel="stylesheet" href="assets/fonts/inter.css"> Powerful TypeScript Strict Mode in Angular: Patterns,… | Viral Patel Studio Skip to main content

Powerful TypeScript Strict Mode in Angular: Patterns, Benefits & Migration Guide 2025

May 27, 2025By Viral Patel

Enable TypeScript strict mode in your Angular project for safer code. Learn all strict flags, common migration errors with fixes, and strict patterns.

Approximately 60% of Angular projects in the wild are running without strict mode. That means null reference errors that only appear at runtime, any types that silently defeat TypeScript's entire purpose, and templates that pass the compiler despite type mismatches. Angular has shipped with strict: true by default since v12. If your project is older — or your team disabled it "to move faster" — this guide shows you exactly how to turn it on, migrate the errors, and write the patterns that prevent them from coming back. By the end you will have a fully strict Angular project, a repeatable migration playbook, and a clear picture of what strict mode actually buys you.


What TypeScript Strict Mode Actually Enables

strict: true is a single flag in tsconfig.json that acts as a shorthand for enabling a collection of individual compiler options. Understanding what each one does is essential before you start a migration — knowing which flag triggered a specific error tells you exactly how to fix it.

The flags enabled by strict: true are:

  • strictNullChecks — null and undefined are no longer assignable to other types. A variable typed as string cannot hold null without explicitly being typed string | null. This is the single highest-impact flag and prevents roughly 40% of production Angular runtime errors.
  • noImplicitAny — Variables and parameters must have an explicit type or a type that TypeScript can fully infer. If TypeScript cannot determine the type, it errors rather than silently falling back to any.
  • strictFunctionTypes — Function parameter types are checked contravariantly, meaning stricter compatibility rules apply when assigning functions to typed variables. This catches subtle callback type mismatches that non-strict mode ignores.
  • strictPropertyInitialization — Class properties must be definitively assigned in the constructor, or explicitly marked with the definite assignment assertion operator (!). This prevents accessing uninitialized class properties at runtime.
  • strictBindCallApply — The .bind(), .call(), and .apply() methods on functions are type-checked against the function's actual signature rather than returning any.
  • useUnknownInCatchVariables (enabled via strict in TypeScript 4.4+) — Caught errors in catch blocks are typed as unknown rather than any, forcing you to check the error type before accessing its properties.

Angular-Specific Strict Additions

Angular's compiler adds its own strict flags in tsconfig.app.json (or tsconfig.json for standalone setups):

{
  "angularCompilerOptions": {
    "strictTemplates": true,
    "strictInjectionParameters": true
  }
}

strictTemplates is Angular's most powerful addition. It type-checks every expression in your HTML templates — property bindings, event bindings, template variables, pipe outputs. Without it, you can pass the wrong type to an @Input(), call a method that doesn't exist in a template, or use a property that is undefined in a template expression, all without any compile-time error.

strictInjectionParameters ensures that every class injected via Angular's DI system has the metadata needed for injection. It catches misconfigured providers and missing @Injectable() decorators that would otherwise produce cryptic runtime errors.


Enabling Strict Mode: The tsconfig.json Setup

Before enabling anything, run a dry-run check to see how many errors currently exist in your project. This is your baseline.

npx tsc --noEmit 2>&1 | grep "error TS" | wc -l

This command compiles your TypeScript without emitting files and counts the type errors. On a project that has never had strict mode enabled, this number might be anywhere from 20 to several thousand. Knowing the number before you start helps your team plan the migration sprint correctly.

The full recommended tsconfig.json for a strict Angular project:

{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": false,
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022", "dom"],
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

Two additional flags worth noting:

  • noUnusedLocals and noUnusedParameters — These are not part of strict but complement it well. Unused variables are common sources of confusion and dead code. Enable them after strict migration is complete.
  • exactOptionalPropertyTypes — A stricter variant of optional properties. Set to false initially; it can be enabled separately once strict mode is stable. It prevents assigning undefined explicitly to an optional property (obj.prop = undefined), which is stricter than most codebases initially expect.

To check only specific files during migration:

npx tsc --noEmit --strict src/app/services/user.service.ts

The 6 Most Common Strict Mode Errors in Angular (and How to Fix Each)

These six error patterns account for the vast majority of strict mode failures in real Angular codebases. Master these fixes and you can handle 90% of what a migration throws at you.

Error 1: Object is possibly 'null' or 'undefined' (TS2531 / TS18047)

This is the most common error after enabling strictNullChecks. It surfaces wherever your code assumes a value is always defined when TypeScript can determine it might not be.

// ❌ Before strict — user could be null from an API call
export class ProfileComponent {
  user: User | null = null;

  getUserName(): string {
    return this.user.name; // TS18047: 'this.user' is possibly 'null'
  }
}

// ✅ Fix 1: Optional chaining + nullish coalescing (preferred)
getUserName(): string {
  return this.user?.name ?? 'Guest';
}

// ✅ Fix 2: Type guard with early return
getUserName(): string {
  if (!this.user) return 'Guest';
  return this.user.name; // TypeScript now knows user is defined here
}

// ✅ Fix 3: Non-null assertion operator (use sparingly)
// Only appropriate when you have certainty the value is defined
// at the point of access — e.g., inside an @if block or after
// a guard check that TypeScript cannot track.
getUserName(): string {
  return this.user!.name;
}

The non-null assertion operator (!) is a last resort. It tells TypeScript "trust me, this is defined" — but if you are wrong, you get the exact runtime error strict mode was meant to prevent. Prefer type guards and optional chaining.

Error 2: Property has no initializer (TS2564 — strictPropertyInitialization)

TypeScript requires that every class property either be initialized in the constructor or explicitly typed to include undefined.

// ❌ Before strict
export class UserComponent {
  @Input() user: User; // TS2564: Property 'user' has no initializer
  private subscription: Subscription; // Same error
}

// ✅ Fix 1: Use Angular input() signals (preferred in Angular 17+)
// input() signals are always initialized — they carry type safety by design
export class UserComponent {
  readonly user = input.required<User>();
  readonly theme = input<'light' | 'dark'>('light'); // with default
}

// ✅ Fix 2: Mark as optional (adds undefined to type)
export class UserComponent {
  @Input() user?: User; // Type becomes User | undefined
}

// ✅ Fix 3: Provide a default value
export class UserComponent {
  @Input() user: User = { name: 'Guest', id: 0, email: '' };
}

// ✅ Fix 4: Definite assignment assertion (when initialized outside constructor)
export class UserComponent {
  @Input() user!: User; // Tell TS: this will be assigned before use
  // Use only for @Input() properties assigned by Angular's DI before ngOnInit
}

The migration to input() signals (Angular 17+) is the cleanest fix for @Input() strict errors and brings additional benefits: inputs are readonly signals, colocated default values are idiomatic, and input.required<T>() removes the need for ! assertions entirely.

Error 3: Implicit 'any' Type (TS7006 — noImplicitAny)

This error fires wherever TypeScript cannot infer a type and you have not provided one. Common locations: function parameters, callback arguments, and variables initialized without assignment.

// ❌ Before strict — 'response' and 'item' implicitly any
processResponse(response) {
  return response.data;
}

const results = apiData.map(item => item.id);

// ✅ Fix: Add explicit types
interface ApiResponse<T> {
  data: T;
  status: number;
}

processResponse(response: ApiResponse<User[]>): User[] {
  return response.data;
}

const results: number[] = (apiData as User[]).map((item: User) => item.id);
// Or better — type the source, let inference flow downstream:
const typedApiData: User[] = apiData;
const results = typedApiData.map(item => item.id); // item inferred as User

The principle: type at the source, let TypeScript infer everywhere else. Avoid typing every intermediate variable — type the inputs and outputs of functions, and TypeScript's inference will handle the rest.

Error 4: Template Type Errors (strictTemplates)

Template errors are unique to Angular and require fixes in either the template or the component class.

// Component
@Component({ template: `<app-user-badge [label]="user?.name" />` })
export class ProfileComponent {
  user: User | null = null;
}

// ❌ Template error if UserBadgeComponent declares:
@Input() label: string; // Not string | null

// ✅ Fix 1: Use nullish coalescing in template
// <app-user-badge [label]="user?.name ?? 'Guest'" />

// ✅ Fix 2: Update @Input type to accept null/undefined
@Input() label: string | null = null;

// ✅ Fix 3: Use input() signal with explicit type
readonly label = input<string | null>(null);

Angular's @if control flow performs type narrowing inside the block, which is the cleanest template-level fix for null checks:

<!-- ✅ TypeScript knows 'user' is defined inside @if -->
@if (user) {
  <app-user-badge [label]="user.name" />
}

Error 5: Service Injection Not Typed (strictInjectionParameters)

// ❌ Service not decorated — no injection metadata
export class AnalyticsService {
  track(event: string): void { /* ... */ }
}

// ✅ Fix: Add @Injectable decorator
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
  track(event: string): void { /* ... */ }
}

// ❌ Constructor injection of unresolvable token
constructor(private service: AbstractService) {} // Error if no provider

// ✅ Fix: Ensure provider exists or use inject()
private readonly service = inject(AbstractService);
// And register provider in component or module:
// providers: [{ provide: AbstractService, useClass: ConcreteService }]

Error 6: Untyped RxJS Chains (noImplicitAny in HTTP calls)

RxJS operators return typed observables, but only when you type the source. Untyped HTTP calls are the most common source of implicit any in Angular services.

// ❌ Before strict — data is any, users is any
this.http.get('/api/users').subscribe(data => {
  this.users = data;
});

// ✅ Fix: Type the HTTP call at the source
this.http.get<User[]>('/api/users').subscribe(users => {
  this.users = users; // users is correctly typed as User[]
});

// ✅ Fully strict service method with typed error handling
loadUsers(): Observable<User[]> {
  return this.http.get<User[]>('/api/users').pipe(
    map(users => users.filter(u => u.active)),
    catchError((error: HttpErrorResponse) => {
      this.errorService.handle(error);
      return of([]);
    })
  );
}

Incremental Migration Strategy for Large Codebases

Enabling strict: true on a 200+ file Angular project all at once is almost never the right move. The error count is overwhelming, the diff is massive, and the risk of regression is high. An incremental phase-based approach lets you ship improvements continuously while keeping the codebase functional.

Phase 1 — noImplicitAny (Lowest Friction)

Start here. noImplicitAny alone rarely produces more than a few dozen errors even on large projects, and each fix is mechanical: add a type annotation.

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Work through errors file by file. For third-party code without types, install @types/package-name or add a local .d.ts declaration. Avoid any as a fix — use unknown if the type is genuinely unknown, then narrow it before use.

Phase 2 — strictNullChecks (Highest Impact)

This phase typically produces the most errors. It also delivers the most runtime bug prevention. Plan for 1–2 sprints on a large codebase.

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

Strategy: work service by service, starting with leaf services (no dependencies on other services) and moving toward components last. Use // @ts-ignore with a // TODO: strict comment as a temporary escape hatch for complex cases — track them in a follow-up ticket, never leave them permanently.

Use VSCode's TypeScript Problems panel to see all errors at once. Filter by file using the search box to tackle one file at a time.

Phase 3 — strictPropertyInitialization

With strictNullChecks in place, this phase is usually fast. Most fixes are either adding ? to optional properties or migrating @Input() decorators to input() signals.

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true
  }
}

This is also a good time to audit your component @Input() properties and decide which should be required vs optional. input.required<T>() from Angular 17+ is the idiomatic answer for required inputs.

Phase 4 — strictTemplates

Template errors are the most Angular-specific and often require understanding component contracts more deeply than pure TypeScript errors.

{
  "angularCompilerOptions": {
    "strictTemplates": true
  }
}

Run ng build (not just tsc) to surface template errors. The Angular compiler reports them with template file and line number. Work component by component — fix the strictest components first (those with the most typed bindings) to build momentum.

Phase 5 — Full strict: true

Once Phases 1–4 are complete, enabling the full strict: true flag should produce minimal new errors (primarily strictFunctionTypes and strictBindCallApply catches). Enable it, run the count, fix the remaining issues, and commit.

# Track progress at each phase
npx tsc --noEmit 2>&1 | grep "error TS" | wc -l

Strict Patterns for Angular Services

The following pattern represents a fully strict Angular service in 2025. It uses signals for state, inject() for dependency injection, and typed RxJS throughout.

import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { tap, catchError, map } from 'rxjs/operators';

export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'editor' | 'viewer';
  active: boolean;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly http = inject(HttpClient);
  private readonly apiBase = '/api/v1';

  // Signal-based state — null-safe by design
  readonly currentUser = signal<User | null>(null);
  readonly isLoggedIn = computed(() => this.currentUser() !== null);
  readonly isAdmin = computed(() => this.currentUser()?.role === 'admin');

  loadUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiBase}/users/${id}`).pipe(
      tap(user => this.currentUser.set(user)),
      catchError((error: HttpErrorResponse) => {
        console.error(`Load user ${id} failed:`, error.message);
        return throwError(() => new Error(`Failed to load user: ${error.message}`));
      })
    );
  }

  loadActiveUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${this.apiBase}/users`).pipe(
      map(users => users.filter(u => u.active)),
      catchError((error: HttpErrorResponse) => {
        console.error('Load users failed:', error.message);
        return throwError(() => error);
      })
    );
  }

  updateUser(id: number, changes: Partial<User>): Observable<User> {
    return this.http.patch<User>(`${this.apiBase}/users/${id}`, changes).pipe(
      tap(updated => {
        // Update signal if this is the current user
        if (this.currentUser()?.id === updated.id) {
          this.currentUser.set(updated);
        }
      })
    );
  }

  logout(): void {
    this.currentUser.set(null);
  }
}

Key strict patterns at work here:

  • signal<User | null>(null) — explicit null in the type, no ambiguity about initial state
  • computed(() => this.currentUser() !== null) — derived state with type inference (boolean)
  • this.http.get<User>() — typed at the source, all downstream operations are type-safe
  • (error: HttpErrorResponse) — explicit error type in catchError, not any
  • inject() instead of constructor injection — cleaner, works in functional contexts

Strict Angular Components: Input Signals and Type Safety

Angular's input() signal API (stable since Angular 17.1) is the cleanest solution to component strict mode compliance. Required inputs, optional inputs with defaults, and transform functions are all first-class patterns.

import {
  Component, ChangeDetectionStrategy,
  input, computed, output
} from '@angular/core';

export interface User {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  avatarUrl?: string;
}

@Component({
  selector: 'app-user-card',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <article class="user-card" [class.user-card--admin]="isAdmin()">
      @if (user().avatarUrl; as avatarUrl) {
        <img [src]="avatarUrl" [alt]="displayName() + ' avatar'" />
      } @else {
        <div class="user-card__avatar-placeholder" aria-hidden="true">
          {{ initials() }}
        </div>
      }

      <div class="user-card__info">
        <h3 class="user-card__name">{{ displayName() }}</h3>
        <p class="user-card__email">{{ user().email }}</p>
      </div>

      @if (showActions()) {
        <div class="user-card__actions">
          <button (click)="editClicked.emit(user())">Edit</button>
        </div>
      }
    </article>
  `
})
export class UserCardComponent {
  // Required input — TypeScript error if parent doesn't provide it
  readonly user = input.required<User>();

  // Optional inputs with defaults — type inferred from default value
  readonly isAdmin = input(false);
  readonly showActions = input(true);

  // Outputs
  readonly editClicked = output<User>();

  // Computed values — types fully inferred
  readonly displayName = computed(() =>
    `${this.user().firstName} ${this.user().lastName}`
  );

  readonly initials = computed(() => {
    const u = this.user();
    return `${u.firstName[0]}${u.lastName[0]}`.toUpperCase();
  });
}

This component is entirely strict-compliant with zero ! assertions and zero any types. The input.required<User>() pattern eliminates the TS2564 property initialization error completely — Angular handles the input assignment and TypeScript knows the value is always defined inside the component's lifecycle.

For more on Angular signals architecture, see the Angular Signals complete guide for design system components.


Strict Templates: Type-Safe HTML Patterns

With strictTemplates: true, Angular's template compiler becomes a full type-checking partner. Here are the key patterns for writing templates that pass strict checks cleanly.

Safe Navigation and Nullish Coalescing

<!-- ✅ Safe navigation operator — returns undefined without throwing -->
<p>{{ user?.address?.city }}</p>

<!-- ✅ Nullish coalescing — provides fallback for null/undefined -->
<p>{{ user?.name ?? 'Anonymous User' }}</p>

<!-- ✅ Combining both for deeply optional data -->
<p>{{ profile?.user?.displayName ?? profile?.user?.email ?? 'Unknown' }}</p>

@if Type Narrowing

Angular's @if control flow (Angular 17+) narrows types inside the block. This is the cleanest way to handle nullable values in templates without template type errors.

<!-- ✅ TypeScript knows 'currentUser' is User (not User | null) inside @if -->
@if (currentUser()) {
  <app-user-profile [user]="currentUser()!" />
}

<!-- ✅ Even better: use @if with 'as' to alias the narrowed value -->
@if (currentUser(); as user) {
  <app-user-profile [user]="user" />
}

Typed @for with track

<!-- ✅ track is required in Angular 17+ @for — use a unique, stable property -->
@for (user of users(); track user.id) {
  <app-user-card [user]="user" />
}

<!-- ✅ @empty block for the null/empty case -->
@for (item of items(); track item.id) {
  <app-item [data]="item" />
} @empty {
  <p class="empty-state">No items found.</p>
}

Pipe Type Safety

Angular pipes preserve types through strict template checking. Use typed pipes to avoid any in template expressions.

<!-- ✅ AsyncPipe returns T | null — handle the null case -->
@if (user$ | async; as user) {
  <span>{{ user.name }}</span>
}

<!-- ✅ SlicePipe, DatePipe, etc. are typed — correct input types required -->
{{ createdAt | date: 'mediumDate' }}

ESLint Rules That Enforce Strict Patterns

TypeScript compiler flags are project-wide enforcement. ESLint rules enforce strict patterns at the individual expression level and flag patterns that technically compile but violate strict idioms.

Add these rules to your .eslintrc.json or eslint.config.js:

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-non-null-assertion": "warn",
    "@typescript-eslint/explicit-function-return-type": "warn",
    "@typescript-eslint/no-unsafe-assignment": "error",
    "@typescript-eslint/no-unsafe-member-access": "error",
    "@typescript-eslint/no-unsafe-call": "error",
    "@typescript-eslint/no-unsafe-return": "error",
    "@typescript-eslint/prefer-nullish-coalescing": "error",
    "@typescript-eslint/prefer-optional-chain": "error"
  }
}

Key rules explained:

  • no-explicit-any: error — disallows using any as a type annotation. Forces unknown + type narrowing for truly unknown values.
  • no-non-null-assertion: warn — warns on ! operator. Warning (not error) because there are legitimate uses, but every instance deserves review.
  • prefer-nullish-coalescing: error — enforces ?? over || for null/undefined checks. Prevents bugs where falsy values like 0 or '' incorrectly trigger the fallback.
  • prefer-optional-chain: error — enforces ?. over && chaining. More readable and strict-mode idiomatic.

Strict Mode and Design Systems: Why It Matters

A design system component library is where strict mode pays the biggest dividends. Components are consumed by dozens of teams and features — a wrong prop type in a core component can cause subtle UI bugs across an entire product.

Component Contracts as Types

With strictTemplates enabled, your component @Input() types become enforceable contracts. If a consuming component passes the wrong type to an input, it fails at compile time — not in production with a broken UI.

// Design system button component — strict types as contracts
@Component({ selector: 'ds-button', ... })
export class ButtonComponent {
  readonly variant = input<'primary' | 'secondary' | 'ghost'>('primary');
  readonly size = input<'sm' | 'md' | 'lg'>('md');
  readonly disabled = input(false);
  readonly loading = input(false);
  readonly clicked = output<MouseEvent>();
}

// ❌ Consumer error — 'danger' is not a valid variant
// <ds-button variant="danger" />
// TS error: Type '"danger"' is not assignable to type '"primary" | "secondary" | "ghost"'

// ✅ Valid usage only — strict templates enforce the contract
// <ds-button variant="secondary" size="lg" />

Design Token Types

Use template literal types to type your design tokens, preventing magic strings and invalid token references:

// Design token type definitions
type ColorScale = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
type ColorToken = `--ds-color-${'brand' | 'neutral' | 'success' | 'warning' | 'error'}-${ColorScale}`;
type SpaceToken = `--ds-space-${1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16}`;
type TypographyToken = `--ds-type-${'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl'}`;

// Usage in a theming service
@Injectable({ providedIn: 'root' })
export class DesignTokenService {
  getColor(token: ColorToken): string {
    return getComputedStyle(document.documentElement)
      .getPropertyValue(token)
      .trim();
  }
}

// ❌ TypeScript error — invalid token name
// tokenService.getColor('--ds-color-brand-950'); // '950' not in ColorScale

// ✅ Valid token reference
tokenService.getColor('--ds-color-brand-500');

This pattern makes it impossible to reference a non-existent design token in TypeScript — the compiler enforces your token vocabulary. For a deeper exploration of this approach, see the design system maturity model guide.


The Strict Mode Payoff: What Changes After Migration

The migration is the painful part. Here is what changes permanently once strict mode is enabled and the codebase is clean.

Runtime error reduction. Teams that migrate to strict mode consistently report a 40–60% reduction in null reference runtime errors within the first few sprints after migration. strictNullChecks alone catches the entire category of "Cannot read properties of null/undefined" errors at compile time.

IDE experience transformation. With no any types defeating type inference, VSCode's autocomplete becomes dramatically more accurate. Every property access, every method call, every function argument shows the correct type. Developers stop guessing at API shapes and start trusting the compiler.

Safe refactoring. Renaming a service method, changing a component input type, or restructuring a data model instantly surfaces every usage that needs updating via compiler errors. In a non-strict codebase, refactoring relies on text search and hoping nothing was missed. In a strict codebase, the compiler finds everything.

Faster onboarding. Types document intent. A new developer looking at user: User | null immediately understands this value might not be present. A service typed as get<User[]>('/api/users') tells the story of what the endpoint returns without reading docs. Strict mode makes code self-documenting.

Better Angular Ivy output. Angular's Ivy compiler uses type information to optimize component compilation. With strictTemplates, Ivy can make stronger assumptions about what code paths are reachable and generate more efficient JavaScript. Dead code elimination improves, and the compiler can perform better tree-shaking on strictly-typed component trees.


Migration Checklist

Use this checklist to track progress on a strict mode migration project:

  1. Baseline error count — Run npx tsc --noEmit 2>&1 | grep "error TS" | wc -l and record the number
  2. Enable noImplicitAny — Fix all implicit any errors; install missing @types/* packages
  3. Enable strictNullChecks — Largest phase; fix with optional chaining, nullish coalescing, and type guards
  4. Migrate @Input() to input() signals — Use Angular 17+ input API for all required inputs
  5. Enable strictPropertyInitialization — Mark genuinely optional properties with ?
  6. Enable strictFunctionTypes — Fix callback type mismatches in RxJS operators and event handlers
  7. Enable strictBindCallApply — Usually produces few errors; fix .bind() / .call() usages
  8. Enable strictTemplates — Run ng build, fix template type errors component by component
  9. Enable strictInjectionParameters — Add @Injectable() to any missing services
  10. Add ESLint rules — Enforce no-explicit-any, prefer-nullish-coalescing, prefer-optional-chain
  11. Enable noUnusedLocals and noUnusedParameters — Clean up dead code revealed by strict migration
  12. Enable full strict: true — Verify final error count is zero; commit and document the migration

Strict Mode Is a One-Time Investment With Permanent Returns

The migration is painful proportional to how much technical debt exists. For a 50-file project, expect a weekend. For a 500-file project, plan 2–3 focused sprints. The teams that skip it consistently pay more in runtime bugs, debugging time, and risky refactors than the migration would have cost.

The mental model shift is important: strict mode is not a constraint on developer productivity. It is the compiler acting as a code reviewer that never sleeps, catches the null dereferences you missed at 4pm on a Friday, and tells you immediately when a refactor breaks a downstream consumer. Every // @ts-ignore you remove during migration is a runtime error you are preventing.

Angular's ecosystem moved to strict defaults in v12. TypeScript's own strict defaults have been stable since 4.x. The direction of the ecosystem is clear — strict mode is the baseline, not the advanced configuration. Enable it incrementally, fix it properly, keep the ESLint rules to enforce the patterns, and you will not look back.