Understanding Angular Compilation Restrictions

TypeScript’s primary advantage lies in its ability to surface defects during development, often at compile time. The compiler’s permissiveness can be adjusted substantially via configuration, ranging from treating plain JavaScript as valid TypeScript to enforcing a highly restrictive type-checking regime. These same principles extend to Angular’s view templates, which comprise the HTML portion of a component.

This guide consolidates the key compilation restriction settings relevant to an Angular project. For each option, we provide a concise explanation of its purpose along with illustrative code demonstrating the practical impact. While this material exists across Angular and TypeScript documentation, it is frequently presented without sufficient clarity or concrete examples.

As of Angular v12, strict mode serves as the default configuration for new CLI-generated projects (previously requiring the `--strict` flag), making it worthwhile to examine these options in detail.

Throughout this discussion, the terms “compilation” and “transpilation” are treated as synonymous.

Content Index

  1. Defining Angular strict mode
  2. The rationale behind Angular strict mode
  3. TypeScript strict mode flags
    1. strictBindCallApply
    2. strictFunctionTypes
    3. strictNullChecks
    4. strictPropertyInitialization
    5. useUnknownInCatchVariables
  4. Further TypeScript restrictions beyond strict mode
    1. allowUnreachableCode
    2. allowUnusedLabels
    3. alwaysStrict
    4. exactOptionalPropertyTypes
    5. noFallthroughCasesInSwitch
    6. noImplicitAny
    7. noImplicitOverride
    8. noImplicitReturns
    9. noImplicitThis
    10. noPropertyAccessFromIndexSignature
    11. noUncheckedIndexedAccess
    12. noUnusedLocals
    13. noUnusedParameters
  5. View template compilation limitations
    1. Basic mode
    2. Full mode
    3. Strict mode
    4. strictInputTypes
    5. strictInputAccessModifiers
    6. strictNullInputTypes
    7. strictAttributeTypes
    8. strictSafeNavigationTypes
    9. strictDomLocalRefTypes
    10. strictOutputEventTypes
    11. strictDomEventTypes
    12. strictContextGenerics
    13. strictLiteralTypes
  6. Summary
  7. Automate it!

Defining Angular strict mode

Angular strict mode applies more stringent constraints throughout the development process. Its components include:

  • activating TypeScript’s strict mode (reference), which introduces multiple compiler-side checks,
  • applying stricter validation to Angular view templates (specifically within the View Engine Compiler),
  • reducing the bundle size budgets relative to Angular’s standard configuration.

The rationale behind Angular strict mode

Code that meets additional restrictions benefits from more comprehensive static analysis, enabling earlier detection of potential defects. This generally leads to a codebase that is simpler to evolve and maintain. It also reduces the likelihood of runtime-only errors surfacing in production.

Angular’s documentation highlights that projects operating in strict mode are better positioned for automatic framework upgrades, as the `ng update` command performs more reliably in such environments.

TypeScript strict mode configuration

The TypeScript compiler offers a top-level `strict` option that bundles several individual flags, each adding specific constraints during compilation. You can opt into the full strict mode or selectively enable only particular flags.

TS strict mode comprises the following flags:

strictBindCallApply

This flag introduces type checking for arguments passed to these built-in JavaScript methods:

Scenario with the flag disabled (compilation succeeds):

fetchProduct(productId: number): void {
 this.httpClient.get<Product>(`/products/${productId}`).subscribe({
   next: this.fetchProductSuccessHandler.bind(this),
   error: this.fetchProductFailHandler.bind(this)
 })
}

fetchProductSuccessHandler(product: number /** invalid product type **/): void {
 // foo
}

Outcome when the flag is enabled (compilation fails):

 Type '(product: number) => void' is not assignable to type '(value: Product) => void'.
        Types of parameters 'product' and 'value' are incompatible.
          Type 'Product' is not assignable to type 'number'.

Recommendation: the `strictBindCallApply` flag should be enabled unconditionally.

strictFunctionTypes

Official documentation describes this flag rather briefly, noting only that it performs more thorough checking of function argument types. In practice, activating this flag means function parameters can no longer behave in a bivariant way.

What exactly is bivariance? A compact definition can be captured with this sentence:

Bivariance: you may substitute either a derived type or a more general type in place of the "X" type. „

Scenario with the flag disabled (compilation succeeds):

function printStringLowercase(value: string): void {
 console.log(value.toLowerCase());
}

type printSomething = (value: string | number) => void;

const printSomethingFn: printSomething = printStringLowercase;

printSomethingFn(12); // runtime error

Outcome when the flag is enabled (compilation fails):

Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

Another scenario (flag off):

interface Vehicle {
 numberOfWheels: number;
}
interface Car extends  Vehicle {
 brand: string;
}

const vehicle: Vehicle = { numberOfWheels: 2};
const car: Car = {numberOfWheels: 4, brand: 'bmw'};

type driveFnType = (car: Vehicle | Car) => void;



function drive(car: Car): void {
 console.log(car.brand.toUpperCase())
}


const typedDriveFn: driveFnType = drive;

typedDriveFn(vehicle);
typedDriveFn(car);

Outcome when the flag is enabled (compilation fails):

Type '(car: Car) => void' is not assignable to type 'driveFnType'.   Types of parameters 'car' and 'car' are incompatible.     Type 'Vehicle | Car' is not assignable to type 'Car'.

Additional details on typing functions in TypeScript are available at this reference.

Recommendation: the `strictFunctionTypes` flag should be enabled unconditionally.

strictNullChecks

This flag is likely to alter the way you write code on a daily basis. Per the documentation:

  • with the flag turned off, the null and undefined types are effectively disregarded by the compiler,
  • with the flag turned on, the null and undefined types are treated as distinct, allowing TypeScript to detect every scenario where these values could appear.

Scenario with the flag disabled (compilation succeeds):

interface Product {
 id: number;
 model: string | undefined;
 brand: string | null;
 colors: string[];
}

const product: Product = {
 id: 1,
 model: undefined,
 brand: null,
 colors: ['red', 'green', 'blue']
}

const modelLength = product.model.length; // runtime error
const brandLength = product.brand.length; // runtime error
const yellowColorLength = 
    product.colors.find(color => color ==='yellow').length; 
    // runtime error

Outcome when the flag is enabled (compilation fails for all 3 declared consts):

[...] Object is possibly 'undefined'.
[...] Object is possibly 'null'.
[...] Object is possibly 'undefined'.

An additional benefit is the ability to identify variables that may be left unassigned. Here is an example:

let productName: string;
console.log(productName.toLowerCase()); // runtime error

Outcome when the flag is enabled (compilation fails):

 Variable 'productName' is used before being assigned.

In essence, this flag aids the TypeScript compiler in handling nullish types, i.e., types that may resolve to null or undefined. Recent TypeScript releases have introduced dedicated syntax for working with such values:

const colors = ['RED', 'GREEN', 'BLUE'];

const myFavouriteColor = 
    colors.find(color => color.toLowerCase() === 'yellow');

// myFavouriteColor has 'string | undefined' type

if(myFavouriteColor) {
 // typescript inherited that in that scope 
 // myFavouriteColor is  defined
 console.log(myFavouriteColor.toUpperCase());
}

// unsafe property access, runtime exception possible
console.log(myFavouriteColor!.toLowerCase());

// safe property access 
// (method will be called only if myFavouriteColor is defined)
console.log(myFavouriteColor?.toLowerCase())

Further reading on nullish value handling is available here:

Recommendation: this flag is highly recommended, especially for greenfield projects.

strictPropertyInitialization

This flag depends on `strictNullChecks`; without it, enabling will trigger an error:

Deeper details about nullish semantics can be found here.

Error: error TS5052: Option 'strictPropertyInitialization' cannot be specified without specifying option 'strictNullChecks'.

When active, this flag mandates that every class property receive an initial value either directly at declaration or within the constructor. Initializing them via a method called from the constructor is not a valid workaround.

Consider this scenario (flag off):

@Component({
 selector: 'app-product',
 template: `
   <p
     #productName
     [class.collapsed]="collapsed"
   > {{ product?.name }}</p>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductComponent implements AfterViewInit {
 @Input() product: ProductDetails;
 @ViewChild('productName') productNameRef: ElementRef<HTMLParagraphElement>;
 collapsed: boolean;


 ngAfterViewInit(): void {
   console.log(this.product.name); 
   // ok if product has been passed via input
   console.log(this.productNameRef.nativeElement);
    // ok if html element with given angular selector exists
   this.collapsed = false; // expand element
 }
}

This code compiles without any problems. If an Angular Input gets passed to this component, all console logs will execute as expected, with no errors raised.

This mirrors a common situation in the Angular component lifecycle: values supplied via @Input, @ViewChild, or similar decorators are not available at the moment the component instance is constructed; they become accessible only at a later lifecycle stage (such as AfterViewInit for @ViewChild). More on the Angular component lifecycle can be found here.

What changes when `strictPropertyInitialization` is enabled?

Property 'product' has no initializer and is not definitely assigned in the constructor.
Property 'productNameRef' has no initializer and is not definitely assigned in the constructor.
Property 'collapsed' has no initializer and is not definitely assigned in the constructor.

Unsurprisingly, compilation fails in this case. When immediate initialization at instance creation isn't feasible, two common approaches exist:

  1. Opt for nullable field types,
  2. Apply the non-null assertion operator,

Approach one (nullish types):

@Input() product?: ProductDetails;
@ViewChild('productName') productNameRef?:      ElementRef<HTMLParagraphElement>;
collapsed?: boolean;

each usage of these properties must account for the possibility of nullish values (e.g., through optional chaining). This is a safe path, but it requires extra effort.

Approach two (non-null assertion):

@Input() product!: ProductDetails;
@ViewChild('productName') productNameRef!: ElementRef<HTMLParagraphElement>;
collapsed!: boolean;

the developer takes on the responsibility of ensuring these fields are initialized at the right time and that no attempt is made to read them before that point.

These strategies can also be combined: declare properties as nullable and then use non-null assertions when accessing them, provided you are certain they have been assigned.

Recommendation: when enabling this flag, the preferred approach is to rely on nullish types rather than non-null assertions.

useUnknownInCatchVariables

This flag was introduced with TypeScript 4.4. As of this writing, Angular 12.1.1 does not yet accommodate TS 4.4 or later.

Before this flag existed, the caught error in a try-catch block was implicitly typed as `any`, leaving no room to change it (given that JavaScript permits throwing any arbitrary value).

function parseJsonToUser(json: string): User | undefined {
 try {
   const { id, name }: { id: number, name: string} = JSON.parse(json);
   return new User({ id, name });

 } catch (error) {
   // 'error' variable has 'any' type here
   console.error(error.message.details.property.foo);
   // accessing properties of error might cause another runtime exception
 }
}

When the flag is active, the `error` variable becomes `unknown`, leading to a compilation failure:

Property 'message' does not exist on type 'unknown'.

Starting with TS 4.4, the type of the caught variable can be explicitly declared as either `any` or `unknown`, independent of the flag's setting. The flag solely determines what is used by default.

Guidance on managing values of type unknown can be found here.

Recommendation: adopt this flag as soon as you move to TypeScript 4.4 or newer.

TypeScript compiler options beyond strict mode

The TypeScript compiler configuration ships with several other useful rules that go beyond what strict mode enables. These options tighten type safety and help catch potential issues early.

allowUnreachableCode

This flag behaves differently from most others — setting it to false adds stricter validation than leaving it at its default state.

Here's how the different values behave:

  • with true, code that can never execute (unreachable code) is simply ignored,
  • with undefined (the default), unreachable code is still ignored just as with true, but the compiler additionally supports editor warnings for such code. IDEs typically show these warnings even when the flag is set to true,
  • with false, unreachable code produces a compilation error.

With the option set to true, compilation passes without problems:

function getArrayLength(array: Array<any> | null | undefined): number {
 if (Array.isArray(array)) {
   return array.length;
 } else {
   return 0;
 }
 // unreachable code
 return 5;
}

Setting it to false changes the outcome:

Unreachable code detected.

Recommendation: Switch this to false unless you're working with legacy code where this would be hard to satisfy.

allowUnusedLabels

Labels are an infrequently used part of JavaScript syntax (and thus TypeScript as well) that pair with the break and continue keywords. They let you name loops and then interrupt or resume execution of a specific loop by referring to that name, even when working with nested structures.

function isInMatrix(matrix: string[][], term: string): boolean {
 loopOverX:
 for (let x = 0; x < matrix.length; x++) { // loop labeled as loopOverX
   const rowLength = matrix[x].length;

   loopOverY:
   for(let y = 0; y < rowLength; y++) { // loop labeled as loopOverY
     if(matrix[x][y] === 'foo') {
       break loopOverX;
     }

     if(matrix[x][y] === 'bar') {
       continue loopOverY;
     }

     if(matrix[x][y] === term) {
       return true;
     }
   }
 }
 return false;
}

JavaScript and TypeScript permit placing a label nearly anywhere (though doing so is almost always pointless and should be considered a bug).

function printUser(user: User): void {
 label1:
 label2:
 label3:
   console.log(`hello ${user.firstName} ${user.lastName}`);
}

When this restriction is active (flag set to false), every unnecessarily defined label triggers an error:

Unused label.

An interesting detail: in the isInMatrix function example, the label (loopOverY) is technically redundant, because dropping it from the break/continue statements would still interrupt or continue the innermost loop. But in that scenario TypeScript allows keeping the label — in our view this improves readability when you've decided to use labels with nested loops.

Recommendation: Configure false. Use labels exclusively for nested loops where you need to interrupt or continue specific subsequent iterations.

alwaysStrict

This option doesn't modify how TypeScript code itself behaves, but it ensures all emitted JavaScript files run in Ecmascript strict mode. Strict mode deserves its own in-depth discussion, but the essentials are:

  • each output *.js file gets its own "use strict" prefix
  • all compliant JavaScript engines evaluate the code more restrictively during runtime — errors that would otherwise be silently ignored in non-strict contexts are now surfaced.

exactOptionalPropertyTypes

Introduced in TypeScript 4.4. At the time this article was written, the current Angular release (12.1.1) didn't yet support TS 4.4+.

To make the purpose of this option clear, here's some background:

type ApplicationSettings {
 theme?: 'Dark' | 'Light'
}

const settingsA: ApplicationSettings = {};
const settingsB: ApplicationSettings = { theme: undefined }

The application settings object (typed as ApplicationSettings) includes a theme field, which can hold 'Dark', 'Light', or undefined.

We construct two objects, settingsA and settingsB, in different ways — the first omits the 'theme' property altogether, while the second explicitly assigns it the value undefined. In typical scenarios, the theme field behaves identically in both:

function applySettings(settings: ApplicationSettings): void {
 if(settings.theme) {
   // same result for both
 }

 if(!settings.theme) {
   // same result for both
 }

 if(settings.theme === undefined) {
   // same result for both
 }

 if(typeof settings.theme === "undefined") {
   // same result for both
 }

 if(settings.theme == null) {
   // same result for both
 }

 const theme = settings.theme; // same result for both
}

But there are contexts in which the two objects are handled differently:

function applySettings(settings: ApplicationSettings): void {
 if("theme" in settings) {
   // different behavior
 }

 if(Object.keys(settings).includes("theme")) {
   // different behavior
 }

 if(settings.hasOwnProperty("theme")) {
   // different behavior
 }
}

The console.log output reveals those differences clearly:

 "settingsA": {}
  "settingsB": { "theme": undefined }

That's exactly the problem the "exactOptionalPropertyTypes" flag was designed to solve. It stops you from explicitly assigning undefined to an optional field — so when an optional property has no value, the resulting object simply has no key for it.

Without the flag (compilation succeeds):

type ApplicationSettings {
 theme?: 'Dark' | 'Light'
}

const settingsA: ApplicationSettings = {};
const settingsB: ApplicationSettings = { theme: undefined }

With the flag enabled (even though the theme field remains optional):

Type 'undefined' is not assignable to type '"Dark" | "Light"'.

Recommendation: Adopt this option as soon as you're on TypeScript 4.4 or newer.

noFallthroughCasesInSwitch

With this option enabled, every case in a switch/case block that contains any statements to run must end with either the break or return keyword (assuming the switch is inside a function). Grouping multiple cases together — that is, cases with no statements between them — remains allowed.

With the option disabled, compilation succeeds:

type Color = 'red' | 'green' | 'yellow';
function applyColor(color: Color): void {
 switch(color) {
   case 'yellow':
     console.log(color);
     // missing 'break' statement
   case 'green':
     return;
 }
}

Enabling "noFallthroughCasesInSwitch" produces this error:

Fallthrough case in switch.

This setting exists to catch accidental omissions of break and/or return statements.

Recommendation: turn on the noFallthroughCasesInSwitch option

noImplicitAny

When a type isn't specified explicitly, TypeScript attempts to deduce it from usage context. If no meaningful type can be inferred, the language falls back to the any type.

Enabling noImplicitAny means that when type inference is impossible and the developer hasn't provided an explicit type either, a compilation error is raised.

Without the flag enabled:

function print(value): void {
 // "value" argument has "any" type here
 console.log(value);
}

After enabling the flag, you get this error:

Parameter 'value' implicitly has an 'any' type.

Recommendation: Use this option in every project. Keep in mind that external libraries without proper typings will require you to define types for them manually.

noImplicitOverride

This option, introduced alongside the override keyword in TypeScript 4.3+, requires that any method or property overridden in a derived class be explicitly marked with override. This prevents subtle bugs where a parent class method gets renamed but inheriting classes continue using the old name.

Example without the flag enabled:

class Car {
 honk(): void {}
}

class SportsCar extends Car {
 override honk(): void {}
}

class DeliveryCar extends Car {
 // missing override keyword
 honk(): void {}
}

With the flag turned on, an error is reported:

This member must have an 'override' modifier because it overrides a member in the base class 'Car'.

Recommendation: enable the flag and get into the habit of always writing the override keyword.

noImplicitReturns

When active, this flag checks every possible execution path within each function. If any path doesn't return the declared type — or, when no return type is declared, some paths return a value while others return nothing — compilation fails.

Without the flag enabled:

function getDateLabel(date: Date | null): string {
 if(date) {
   return date.toLocaleDateString('en-US')
 }
 // missing return statement when date is null
}


function getPageTitle(platform: 'android' | 'ios' | 'web'): string {
 switch(platform) {
   case 'android':
     return 'Hello android!'
   case 'ios':
     return 'Hello ios!'
   // missing 'web' case and/or default case
 }
}

function isAdult(age: number): boolean {
 if (age >= 18) {
   return true;
 }
 false; // missing 'return' statement
}

Enabling the flag produces errors for each of the functions above:

Not all code paths return a value

The same error appears when you don't explicitly declare a return type, let TypeScript infer it, but still don't have every path returning a value.

Recommendation: always turn on the noImplicitReturns flag.

noImplicitThis

With this flag set, an error is raised whenever this lacks an explicit type annotation and TypeScript can't infer it from the surrounding context.

Here's a deliberately incorrect example:

class ComplexValidator {
 private static DEFAULT_MINIMUM_ARRAY_LENGTH = 10;
 static minimumArrayLengthValidator(minimumArrayLength?: number): ValidatorFn {
   return function(control: AbstractControl): ValidationErrors | null {
     const controlValue = control.value;
     const minValue = minimumArrayLength ?? this.DEFAULT_MINIMUM_ARRAY_LENGTH;
     return (Array.isArray(controlValue) && controlValue.length < minValue) ? { minArrayLength: true } : null;
   }
 }
}

Inside the method, we define a new function (a classic function, not an arrow function, meaning the value of this depends on how it gets invoked).

Enabling the flag rightfully produces an error:

'this' implicitly has type 'any' because it does not have a type annotation.

It's worth remembering that you can annotate the this parameter directly, allowing functions to be callable only within a specific context. Add the "strictBindCallApply" flag alongside it, and you gain the ability to change types freely while still getting strict checks.

class Car {
 honk(): void {
   console.log('honk honk');
 }
}
function withTypedThis(this: Car, name: string): void {}

// ok
withTypedThis.call(new Car(), 'foo');

// Argument of type 'Date' is not assignable to parameter of type 'Car'
withTypedThis.call(new Date(), 'bar');

Recommendation: Enable the noImplicitThis flag.

noPropertyAccessFromIndexSignature

If some fields are declared with an "index signature", then without this flag you can reference any property using dot notation (e.g. "foo.bar"), even if that property was never defined:

interface HeaderStyles {
 // two most important style properties that have to be set
 height: string;
 display: 'block' | 'none';
 // other styles
 [key: string]: string;
}


const styles: HeaderStyles = {
 height: '60px',
 display: 'block',
 padding: '10px'
}

const display = styles.display;
const display2 = styles['display'];
const padding = styles.padding;
const padding2 = styles['padding'];
const margin = styles.margin;
const margin2 = styles['margin'];

With the "noPropertyAccessFromIndexSignature" option enabled, properties defined via an "index signature" can only be accessed using "index signature" syntax.

This guarantees that the dot notation will never silently refer to a property that could be undefined.

With the flag active:

Property 'padding' comes from an index signature, so it must be accessed with ['padding'].
Property 'margin' comes from an index signature, so it must be accessed with ['margin'].

Recommendation: Always enable the noPropertyAccessFromIndexSignature flag.

noUncheckedIndexedAccess

This option works together with "strictNullChecks". Applying it causes fields typed with an "index signature" of type "X" to be inferred as type "X | undefined".

interface HeaderStyles {
 // two most important style properties that have to be set
 height: string;
 display: 'block' | 'none';
 // other styles
 [key: string]: string;
}


const styles: HeaderStyles = {
 height: '60px',
 display: 'block',
 padding: '10px'
}

styles.padding.toUpperCase();

With the flag enabled:

Object is possibly 'undefined'.

Recommendation: always enable the noUncheckedIndexedAccess flag.

noUnusedLocals

The mechanics are straightforward — any declared local variable that never gets used triggers an error. This also applies to modules that are imported but never referenced.

A file with unused declarations:

import { Input } from '@angular/core';

function sayHello(): void {
 const applicationName = 'Foo';
 console.log(`Hello Foo`);
}
'Input' is declared but its value is never read.
'applicationName' is declared but its value is never read.

Recommendation: Give it a shot. Tooling that auto-removes unused imports (WebStorm's default Ctrl+Alt+O, for instance) makes this painless.

noUnusedParameters

Just like "noUnusedLocals", this forbids declaring function arguments that are never used.

// error: 'applicationName' is declared but its value is never read.
function sayHello(applicationName: string): void {
 console.log(`Hello Foo`);
}

Recommendation: Always use this flag — eliminating unnecessary arguments improves code readability, among other benefits.

Understanding Angular template compilation restrictions

The Angular view template compiler provides three distinct modes for type-checking variables referenced within templates. These modes are configured through specific compiler options:

Basic:

This mode operates under the following configuration:

"angularCompilerOptions": {
 "fullTemplateTypeCheck": false,
 "strictTemplates": false,
 ...
}

Variable references are checked only to confirm that the referenced variables exist as properties on the component class and that the nested attributes being accessed are present on those objects.

Consider this example:

@Component({
 selector: 'app-child',
 template: '<div> {{  street.houseNumbers.length }}</div>',

})
export class ChildComponent {
 @Input() street: { houseNumbers: number[], length: number}
}


@Component({
 selector: 'app-root',
 template: `
  <app-child [street]="user.address.city"></app-child>`
})
export class AppComponent {
 user = {
   address: {
     city: 'foo'
   }
 }
}

The checks performed include:

  • confirming that "user" exists as a field in the component class,
  • confirming that "user" is an object containing the "address" field,
  • confirming that "address" is an object containing the "city" field

What is not verified is whether the type of "user.address.city" is assignable to the input type "street" of the "app-child" component (it is not). The build completes successfully, but a runtime error is inevitable:

Cannot read property 'length' of undefined

Additional aspects left unchecked during compilation in this mode:

  • variables inside "embedded" views (for instance, variables utilized in *ngIf, *ngFor, <ng-template> are invariably typed as "any"). The subsequent example compiles without issues, and both "fruit" and "user" are treated as "any".
@Component({
 selector: 'app-root',
 template: `
 <div *ngFor="let fruit of fruits">
   {{ fruit.foo.bar.baz }}
   {{ user.foo.bar.baz }}
 </div>
 `
})
export class AppComponent {
 fruits = ['apple', 'banana']
 user = {
   firstName: 'foo'
 }
}
  • types for template references (#refs), values produced by pipes, and the $event payloads from any event emitters are consistently assigned the type "any".

Full mode:

This mode is configured with the following flags:

"angularCompilerOptions": {
 "fullTemplateTypeCheck": true,
 "strictTemplates": false,
 ...
}

Several enhancements are introduced compared to the basic mode:

  • variables within "embedded" views (e.g., those inside *ngIf, *ngFor, <ng-template> blocks) now have their types properly deduced and validated,
  • the types of values returned from pipes are now deduced and validated,
  • local references (#refs) pointing to directives and pipes have their types accurately deduced and validated (with the exception of generic parameters),

In this example, the local variable "fruit" remains an "any", yet "user" gets its proper type.

@Component({
 selector: 'app-root',
 template: `
 <div *ngFor="let fruit of fruits">
   {{ fruit.foo.bar.baz }}
   {{ user.foo }}
 </div>
 `
})
export class AppComponent {
 fruits = ['apple', 'banana']
 user = {
   firstName: 'foo'
 }
}

This mode will trigger an error during the build for the following:

 Property 'foo' does not exist on type '{ firstName: string; }'

Strict mode:

This mode uses the following flag configuration:

"angularCompilerOptions": {
 "fullTemplateTypeCheck": true,
 "strictTemplates": true,
 ...
}

Enabling "strictTemplates" to "true" will always take precedence over the "fullTemplateTypeCheck" setting (meaning "fullTemplateTypeCheck" can be safely ignored in this case).

Beyond everything offered in full mode, this mode adds:

  • validation of input type compatibility against the assigned expressions in templates for components and directives (this check also respects the strictNullChecks flag referenced in the Typescript discussion),
  • type inference for local variables declared within embedded views (like the loop variable in an *ngFor directive),
  • type inference for the $event value from component outputs, DOM events, and angular animations,
  • type inference for references (#refs) on DOM elements based on their tag name (e.g., <span #spanRef> is typed as HTMLSpanElement),
@Component({
 selector: 'app-child',
 template: `My name is {{ name }}`,

})
export class ChildComponent {
 @Input() name: string;
}

@Component({
 selector: 'app-root',
 template: `
   <app-child [name]="applicationName"></app-child>
   <div *ngFor="let fruit of fruits">
     {{ fruit.foo.bar }}
   </div>
 `
})
export class AppComponent {
 applicationName: string | undefined = 'foo';
 fruits = ['apple', 'banana'];
}

With the strictNullChecks flag also active, assigning "applicationName" to the "name" input produces this error:

Type 'string | undefined' is not assignable to type 'string'

The local variable "fruit" in this mode gets its correct type (string) inferred:

Property 'foo' does not exist on type 'string'.

When both strictNullChecks and strictTemplates are active, the ubiquitous async pipe deserves special mention. Its "transform" method has the following signature (as an overload):

transform<T>(obj: Observable<T> | Subscribable<T> | Promise<T>): T | null;
transform<T>(obj: null | undefined): null;
transform<T>(obj: Observable<T> | Subscribable<T> | Promise<T> | null | undefined): T | null;

Therefore, for this code snippet:

@Component({
 selector: 'app-child',
 template: `My name is {{ name }}`,

})
export class ChildComponent {
 @Input() name: string;
}

@Component({
 selector: 'app-root',
 template: `
   <app-child [name]="applicationName$ | async"></app-child>
 `
})
export class AppComponent {
 applicationName$ = of('foo');
}

we encounter an error:

Type 'string | null' is not assignable to type 'string'.

This is because the expression "applicationName$ | async" yields a value of type "string | null". Consequently, every input that receives its value through the async pipe must be declared as accepting nullable types.

It's worth noting that the influence of the strictNullChecks flag on input verification can be managed via the strictNullInputTypes flag, which will be covered later.

Recommendation: We strongly advise against using the basic mode. Instead, we highly recommend adopting the strict mode, as it will prevent numerous potential issues.

strictInputTypes

This setting governs the verification of input types. When not explicitly set, its default value is derived from the "strictTemplates" flag.

When assigned a value of "true", the types of expressions bound to inputs are verified, as demonstrated under the strict mode section. A value of "false" will bypass this validation entirely (even if strictTemplates is simultaneously enabled).

Remember that the strictness of this check (regarding nullable types) also depends on the strictNullChecks flag and the strictNullInputTypes flag, both discussed later in this article.

Recommendation: Always keep this verification active (either by setting it directly to true, or indirectly via the strictTemplates flag).

strictInputAccessModifiers

This option determines whether field access modifiers (private/protected/readonly) are enforced during input assignments.

@Component({
 selector: 'app-child',
 template: ``
})
export class ChildComponent {
 @Input() readonly age: number;
 @Input() private firstName: string;
 @Input() protected lastName: string;
}

@Component({
 selector: 'app-root',
 template: `
   <app-child [age]="18" [firstName]="'foo'" [lastName]="'bar'"></app-child>
 `
})
export class AppComponent {}

Without the strictInputAccessModifiers flag, the example above compiles fine (with no runtime issue). With the flag active, we get the corresponding errors:

Cannot assign to 'age' because it is a read-only property.
Property 'firstName' is private and only accessible within class 'ChildComponent'.
Property 'lastName' is protected and only accessible within class 'ChildComponent' and its subclasses.

Adding @Input to a field marked as readonly/private/protected is typically a design mistake (since it exposes those fields to external mutation at any time, which defeats the purpose of these modifiers). Only this dedicated flag catches such misuse.

Recommendation: This flag is not automatically activated by strictTemplates, so it requires explicit enabling! We suggest turning it on (and, of course, avoiding the mentioned access modifiers on @Input fields).

strictNullInputTypes

This flag decides whether the strictNullChecks setting is applied during input type validation. If left unspecified, its default value follows the "strictTemplates" flag.

Let's revisit the async pipe example:

@Component({
 selector: 'app-child',
 template: `My name is {{ name }}`,

})
export class ChildComponent {
 @Input() name: string;
}

@Component({
 selector: 'app-root',
 template: `
   <app-child [name]="applicationName$ | async"></app-child>
 `
})
export class AppComponent {
 applicationName$ = of('foo');
}

With "strictNullChecks" active and the default values for "strictInputTypes" and "strictNullInputTypes" (which could be omitted, causing them to inherit from "strictTemplates"):

{
 ...
 "compilerOptions": {
   ...
   "strictNullChecks": true,
 },
 "angularCompilerOptions": {
   "strictTemplates": true,
   "strictInputTypes": true,
   "strictNullInputTypes": true
 }
}

the same error appears as before, because the async pipe's return type includes null:

Type 'string | null' is not assignable to type 'string'.

However, if this flag is set to "false" while "strictNullChecks", "strictTemplates", and "strictInputTypes" remain enabled:

{
 ...
 "compilerOptions": {
   ...
   "strictNullChecks": true,
 },
 "angularCompilerOptions": {
   "strictTemplates": true,
   "strictInputTypes": true,
   "strictNullInputTypes": false
 }
}

the build succeeds, as "string" matches "string" and nullability is disregarded.

Recommendation: Disabling this flag is not generally recommended, though it might be required for legacy projects (where inputs are non-nullable) or when integrating libraries whose components lack nullable support.

strictAttributeTypes

This flag governs type checks for inputs assigned via "text attributes", as opposed to standard bindings. If not manually set, its default value aligns with the "strictTemplates" flag.

Typically, we bind attributes (including component and directive inputs) using square brackets "[]", signaling to the Angular compiler that the right-hand side is an expression requiring evaluation (often a simple variable reference).

Alternatively, an input's value can be set using a plain HTML attribute (noting that all such attributes are interpreted as strings). When the attribute name matches the input name, the input gets that value.

With the strictAttributeTypes flag turned off, the next example compiles without issue:

@Component({
 selector: 'app-child',
 template: `<span *ngIf="weight">{{ weight.toFixed(2) }}</span>`,

})
export class ChildComponent {
 @Input() firstName: string;
 @Input() lastName: string;
 @Input() weight: number;
}

@Component({
 selector: 'app-root',
 template: `
   <app-child [firstName]="'foo'" lastName="bar" weight="18"></app-child>
 `
})
export class AppComponent {}

This results in the "weight" input receiving "18" as a string (not a number!), leading to a runtime failure:

weight.toFixed is not a function

Enabling the flag allows the compiler to detect this mistake:

Type 'string' is not assignable to type 'number'.

Setting inputs without square brackets is only permissible for inputs typed as strings (or string-based enums).

Recommendation: Activate this flag (and don't deactivate it when using strictTemplates).

strictSafeNavigationTypes

What are "safe navigation" operations? They represent Angular's equivalent of Typescript's Optional Chaining within templates. As an example:

@Component({
 selector: 'app-root',
 template: `
   <p> {{ user?.address?.street }} </p>
 `
})
export class AppComponent {
 user: User = {
   address: {
     street: 'Sesame'
   }
 };
}

When this flag is off, any safe navigation usage results in the value being considered "any". When enabled, the correct type is deduced. If not manually set, it defaults to the "strictTemplates" flag's value.

Without this flag, the compiler only checks that "address" is a property of "user", but still treats the result as "any", allowing access to non-existent members like "bar.baz".

@Component({
 selector: 'app-root',
 template: `
   <p> {{ user?.address.bar.baz }} </p>
 `
})
export class AppComponent {
 user: User = {
   address: {
     street: 'Sesame'
   }
 };
}

The error surfaces only at runtime ("Cannot read property 'baz' of undefined"). With the flag active, the return type of the safe navigation operator is properly inferred, catching the mistake during compilation:

Property 'bar' does not exist on type '{ street: string; }'.

Recommendation: Enable this flag (and keep it on when using strictTemplates).

strictDomLocalRefTypes

This flag controls the type inference for template references applied to DOM elements. If not explicitly set, its value defaults to the "strictTemplates" flag. Our experiments indicate that without enabling "strictTemplates", reference types remain un-inferred irrespective of this flag's value.

Here's an example with strictDomLocalRefTypes disabled (while strictTemplates is on):

@Component({
 selector: 'app-root',
 template: `
   <input type="number" #inputRef>
   <span>
     Input type: {{ inputRef.type.toUpperCase() }}
     Invalid prop: {{ inputRef.foo.bar.baz }}
   </span>
 `
})
export class AppComponent {}

The build succeeds, but a runtime error occurs ("Cannot read property 'bar' of undefined").

When both flags are active (so the reference on "input" is typed as "HTMLInputElement"), a compilation error is raised:

Property 'foo' does not exist on type 'HTMLInputElement'.

Recommendation: Activate this flag (and keep it on when using strictTemplates).

strictOutputEventTypes

This flag manages the type inference for the `$event` payload from component/directive outputs and angular animations. Its default, when unset, mirrors the "strictTemplates" flag.

Example with the flag off:

@Component({
 selector: 'app-child',
 template: ``,

})
export class ChildComponent {
 @Output() numberOutput = new EventEmitter<number>();
}

@Component({
 selector: 'app-root',
 template: `<app-child (numberOutput)="onNumberOutput($event.foo.bar.baz)"></app-child>`
})
export class AppComponent {
 onNumberOutput(value: number): void {}
}

Compilation is successful, but a runtime error appears after the first event fires ("Cannot read property 'bar' of undefined").

With the flag active, $event is correctly typed (as "number"), resulting in a compile-time error:

Property 'foo' does not exist on type 'number'.

Recommendation: Activate this flag (and keep it on when using strictTemplates).

strictDomEventTypes

Similar to the strictOutputEventTypes flag, this one handles $event type inference, but for native DOM events. Its default value follows the "strictTemplates" flag when not set manually.

Example with the flag off:

@Component({
 selector: 'app-root',
 template: `<input type="text" (mouseenter)="$event.foo.bar">`
})
export class AppComponent {}

Compilation proceeds, but a runtime error manifests after the first event ("Cannot read property 'bar' of undefined").

With the flag on, the $event type is inferred as "MouseEvent", and a compilation error is triggered:

Property 'foo' does not exist on type 'MouseEvent'.

Recommendation: Activate this flag (and keep it on when using strictTemplates).

strictContextGenerics

This flag pertains to generic type parameters for components. When disabled, any generic type used in a component is treated as "any" during template type inference. When enabled, generic types are properly resolved. Its default value, if not explicitly set, is inherited from the "strictTemplates" flag.

Examine this scenario:

@Component({
 selector: 'app-child',
 template: `
   {{ value.length }} <!-- OK -->
   {{ value.foo.bar.baz }}  <!-- ERROR -->
 `
})
export class ChildComponent<T extends Array<any>> {
 @Input() value: T;
}

The "value" property has the type "T", confirming it's an array. However, with the flag off, "value" is seen as "any" within the template, making runtime exceptions easy to trigger.

With the flag on, a compilation error appears:

Property 'foo' does not exist on type 'T'.

The compiler accepts the use of 'length', since it's a universal array property.

Recommendation: Enable this flag (and don't disable it when using strictTemplates).

strictLiteralTypes

This flag dictates whether object and array literals declared directly in templates get a proper type (when off, they default to "any"). If unset, its value defaults to the "strictTemplates" flag.

Example with the flag off:

@Component({
 selector: 'app-root',
 template: `
   {{ { firstName: 'foo '}.foo.bar.baz  }}
   {{  ['foo', 'bar'].foo.bar.baz  }}
 `
})
export class AppComponent {}

We define two literals in the template (an object with a 'firstName' field and an array of two strings). Both are considered 'any', permitting access to non-existent properties (resulting in runtime errors).

With the flag on, these errors are flagged during compilation:

Property 'foo' does not exist on type '{ firstName: string; }'.
Property 'foo' does not exist on type 'string[]'.

Recommendation: Enable this flag (and keep it on when using strictTemplates).

Recap

We have now examined the full set of configuration options available. Each flag was intentionally treated in isolation, but in practice they form an interconnected system where choices overlap and influence one another.

Determining the optimal configuration for a given codebase — or for a team, since the setup itself can be shared — often requires iterative experimentation. A sensible default, however, is to err on the side of stricter rules whenever feasible.

Integration with CI

To strengthen the verification workflow, including checks for compile-time correctness, we recommend adding a project build step to your continuous integration pipeline. As highlighted in the introduction, this allows issues to surface at the earliest possible stage.

Strict compilation policies complement other quality gates, such as deep static analysis or automated test suites. Catching a defect through any automated channel delivers substantial savings in both time and cost when compared to discovering it during manual QA, or worse, after users have encountered it in production.