One of the main profits of using Typescript is the possibility of catching some of the bugs right on the spot (or during the compiling process). Depending on the chosen approach, it can be quite liberal (in particular, it may treat any pure JS code as valid TS code),furthermore , by modifying the compiler configuration, we have a wide spectrum of restrictiveness levels for the transpiled code. It also applies to the compilation of Angular view templates (the HTML part of Angular components).
The aim of this article is to gather information about the most important compilation restriction settings for an Angular project in one place.,There will be a brief description of the restriction itself for each option and a sample code snippet that shows the difference deriving from the option. The information presented below is generally available in Angular and Typescript documentation, but in most cases it is not explained clearly and lacks examples.
Currently in the latest version of Angular (v12) strict mode has become the default option enabled when generating a new project using the CLI ( an additional `–strict` flag was required for older versions), so that’s another good reason to look into the options available to us for a moment.
In this article, the terms “compilation” and “transpilation” will be used interchangeably and equivalently (in the context of this article).
Table of contents
- What is Angular strict mode?
- What is the purpose of Angular strict mode?
- Typescript strict mode flags
- Additional TS restrictions not included in strict mode
- Restrictions on the compilation of Angular view templates
- Summary
- Automate it!
What is Angular strict mode?
Angular strict mode is a mode that activates tighter restrictions during application development. This consists of:
- enabling Typescript strict mode (details), thus enabling various restrictions for the TS compiler,
- enabling various restrictions for Angular view templates (more precisely: restrictions for Angular View Engine Compiler),
- lowering the “bundle size budgets” value (compared to Angular’s default settings).
What is the purpose of Angular strict mode?
Code that meets additional restrictions is susceptible to more thorough static code analysis (so we are able to catch more bugs during the development phase). In general, the project becomes easier to develop and maintain. We also limit the number of bugs which otherwise could only appear during the application runtime.
The Angular documentation states that it is safer and more accurate for projects developed in strict mode to use`ng update` command to automatically update the framework version.
Typescript strict mode flags
The Typescript compiler configuration includes the “strict” option, which is equivalent to enabling various flags responsible for adding further restrictions during code transpilation. We can either enable full strict mode or only some selected flags.
TS strict mode consists of:
strictBindCallApply
Enabling this flag adds verification of argument types of the following built-in Javascript functions:
Example without the enabled flag (transpilation passes):
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
}
The result with the enabled flag (transpilation 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: always use the `strictBindCallApply` flag.
strictFunctionTypes
The official documentation of this flag is very vague and only informs about a more precise verification of function argument types. A more detailed explanation notes that function arguments cannot be bivariant after the activation of this flag.
What are bivariant types? A full explanation can be summarized by the following quote:
“Bivariance: you can use both derivative and superior types instead of the “X” type. “
Example without the enabled flag (transpilation passes):
function printStringLowercase(value: string): void {
console.log(value.toLowerCase());
}
type printSomething = (value: string | number) => void;
const printSomethingFn: printSomething = printStringLowercase;
printSomethingFn(12); // runtime error
The result with the enabled flag (transpilation fails):
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
Another example (without the flag):
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);
The result with the enabled flag (transpilation 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'.
Find more information about typing functions in Typescript, here.
Recommendation: always use the `strictFunctionTypes` flag.
strictNullChecks
This is probably one of the flags that modifies the code you write every day. According to the documentation :
- when the flag is disabled, null and undefined types are ignored by the interpreter,
- when the flag is enabled, null and undefined types are distinguishable, so Typescript will recognize all cases in which these values may appear.
Example without the enabled flag (transpilation passes):
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
The result with the enabled flag (transpilation fails for all 3 declared consts):
[...] Object is possibly 'undefined'.
[...] Object is possibly 'null'.
[...] Object is possibly 'undefined'.
Another beneficial side effect is also the detection of variables that are possibly unassigned. Example:
let productName: string;
console.log(productName.toLowerCase()); // runtime error
The result with the enabled flag (transpilation fails):
Variable 'productName' is used before being assigned.
In other words, this flag supports the Typescript interpreter regarding nullish types (types which can take the values – null or undefined). Newer versions of the language include an additional syntax that is used specifically for handling 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())
More information on ways to handle nullish values can be found here:
Recommendation: we strongly encourage you to use this flag if possible (and especially when creating a new project).
strictPropertyInitialization
Enabling this flag requires the “strictNullChecks” to be enabled first. Otherwise, an error occurs:
More information about nullish itself can be found here.
Error: error TS5052: Option 'strictPropertyInitialization' cannot be specified without specifying option 'strictNullChecks'.
Enabling this flag forces you to initialize (assign values to) all class attributes in their declaration or in the constructor (it is not possible to initialize these values in a method triggered directly in the constructor).
Let’s look at the example below (without the enabled flag):
@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
}
}
Transpilation of such code will succeed. Assuming an angular Input is passed to this component all console logs will execute correctly (no error will occur).
This is a typical example from the Angular component lifecycle, where the values set using @Input, @ViewChild and the other similar decorators are not available while the component class instance is created, instead they become available at one stage of the component lifecycle (e.g. AfterViewInit for @ViewChild). More about the Angular component lifecycle can be found here.
What happens when we enable the “strictPropertyInitialization” flag?
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.
As expected, the code does not pass the compilation process successfully. In the case where it is not possible to initialize the value when the class instance is created, there are two solutions:
- Changing field types to nullable,
- Using non-null assertion operator,
First option (nullish):
@Input() product?: ProductDetails;
@ViewChild('productName') productNameRef?: ElementRef<HTMLParagraphElement>;
collapsed?: boolean;
it is necessary to handle these values as nullable in every place where you use them (e.g. by optional chaining). This is a safe option but increases the amount of work needed.
Second option (non-null assertion):
@Input() product!: ProductDetails;
@ViewChild('productName') productNameRef!: ElementRef<HTMLParagraphElement>;
collapsed!: boolean;
the programmer takes responsibility for ensuring that these values are initialized in a timely manner and that no attempt is made to invoke their values until then.
These solutions can be combined, i.e., set types to nullable, and use non-null assertion when referring to values (while being sure they are already set!).
Recommendation: In case of enabling this flag , we recommend using the approach with nullish types and avoiding the use of non-null assertion.
useUnknownInCatchVariables
This flag has been available in Typescript since version 4.4. At the time of writing this article, the latest version of Angular (12.1.1) does not yet support TS 4.4+.
Before enabling this flag, the try-catch block error was marked as ‘any’ and there was no possibility to change this (because JS itself allows you to throw any value as an exception).
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 turned on, the same ‘error’ variable is marked as ‘unknown’, so we get a transpilation error:
Property 'message' does not exist on type 'unknown'.
Regardless of the flag settings , starting from TS 4.4 we will be able to type errors explicitly as “any” or “unknown”. The flag only affects the default type.
More information on how to deal with unknown type can be found here.
Recommendation: We encourage you to use this flag as soon as you start using Typescript 4.4+.
Additional TS restrictions not included in strict mode
The Typescript compiler configuration also contains a number of other interesting rules including additional restrictions:
allowUnreachableCode
This is one of the flags that, when set to false, imposes more restrictions than when its value is true.
Depending on the value of this flag:
- when true, unreachable code (the one that will never be executed) is ignored,
- when undefined (default value), typescript compiler also ignores unreachable true (just like with true value), but it also provides support for displaying warnings in the code editor. Popular IDEs also do that when the flag is set to true,
- when false, unreachable code causes a compilation error.
Example with the flag set to true (compilation will succeed):
function getArrayLength(array: Array<any> | null | undefined): number {
if (Array.isArray(array)) {
return array.length;
} else {
return 0;
}
// unreachable code
return 5;
}
With the flag set to false:
Unreachable code detected.
Recommendation: Set this flag to false, unless you are dealing with legacy code where this is troublesome.
allowUnusedLabels
Labels are a rarely used element of JavaScript syntax (and therefore also Typescript) working together with break and continue keywords, allowing to identify loops with their names (labels) and to interrupt/continue their execution (by referring to a particular loop, even if the loop is nested within a loop).
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/Typescript allows us to define a label almost anywhere (which of course usually makes no sense and should automatically be considered a bug)
function printUser(user: User): void {
label1:
label2:
label3:
console.log(`hello ${user.firstName} ${user.lastName}`);
}
With the restriction enabled (flag value set to false), an error is received for each redundantly defined label:
Unused label.
Interesting fact: referring to the example of the isInMatrix function – the label (loopOverY) is optional, because if you omit it in the break/continue keywords, the execution of the most nested loop will be interrupted/continued anyway. However, in this case Typescript allows us to keep this label (which in our opinion improves readability when we already decide to use labels in nested loops).
Recommendation: Set the flag to false. Use labels only for nested loops and the need to interrupt/continue their subsequent iterations.
alwaysStrict
This flag has no direct effect on a code written in Typescript, but it makes all files compiled to JavaScript using Ecmascript strict mode. Ecmascript strict mode itself is a material for a separate article, but in short:
- a “use strict” prefix is added to every *.js file
- most Javascript engines (i.e. all compatible with this mode) interpret the JS code in a more restrictive way (during runtime), among other things, some of the errors that in “normal” mode would be ignored in this case are dropped .
exactOptionalPropertyTypes
This flag has been available in Typescript since version 4.4. At the time of writing this article, the latest version of Angular (12.1.1) does not yet support TS 4.4+.
To explain the flags validity, let’s outline some context:
type ApplicationSettings {
theme?: 'Dark' | 'Light'
}
const settingsA: ApplicationSettings = {};
const settingsB: ApplicationSettings = { theme: undefined }
The application settings object (of type ApplicationSettings) has a theme field that can take values: ‘Dark’, ‘Light’ or undefined.
We define settingsA and settingsB objects in two different ways. In the first case we omit the ‘theme’ key, while in the second we explicitly set it to undefined. In most cases, the theme field in both objects will be interpreted the same way:
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
}
However, there are cases where the two objects will be interpreted 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 itself clearly shows us the differences:
"settingsA": {}
"settingsB": { "theme": undefined }
To avoid this type of mismatch, the “exactOptionalPropertyTypes” flag was introduced in Typescript 4.4. Its purpose is to prevent optional fields from being explicitly defined with an undefined value (so that if there is no value in an optional field, there is no defined key for that value in the result object).
Before enabling the flag (compilation passes successfully):
type ApplicationSettings {
theme?: 'Dark' | 'Light'
}
const settingsA: ApplicationSettings = {};
const settingsB: ApplicationSettings = { theme: undefined }
When the flag is turned on (although still an optional theme field):
Type 'undefined' is not assignable to type '"Dark" | "Light"'.
Recommendation: We recommend using this flag as soon as you start using Typescript 4.4+
noFallthroughCasesInSwitch
Enabling this flag means that each case in switch/case that has any instructions to be executed (grouping several cases one after another is still allowed) must end with the break or return keyword (if the switch/case syntax is inside a function).
Example with disabled flag (compilation passes):
type Color = 'red' | 'green' | 'yellow';
function applyColor(color: Color): void {
switch(color) {
case 'yellow':
console.log(color);
// missing 'break' statement
case 'green':
return;
}
}
If the “noFallthroughCasesInSwitch” flag is enabled, the following error occurs:
Fallthrough case in switch.
This flag is to help avoid accidentally omitting the break and/or return keywords.
Recommendation: enable noFallthroughCasesInSwitch flag
noImplicitAny
In Typescript, when a type is not explicitly defined, the interpreter tries to infer it from the context. If it fails to narrow down the possible type in any way, it marks the type as any.
The enabled noImplicitAny flag causes the inability to infer the type from context and the programmer’s failure to explicitly define the type to result in a compilation error.
Example without the flag enabled:
function print(value): void {
// "value" argument has "any" type here
console.log(value);
}
Enabling the flag causes the following error:
Parameter 'value' implicitly has an 'any' type.
Recommendation: We recommend using this flag in every project. Note the need to define types for external libraries that do not provide the right typing.
noImplicitOverride
This is a flag that, along with the new override keyword, appeared in Typescript 4.3+. When enabled, each overriding of a method or field in inheriting class must be preceded with override keyword. Thanks to this we can avoid situations when e.g. we change the name of method in parent class without changing the name in inheriting classes.
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 {}
}
Error when flag is enabled:
This member must have an 'override' modifier because it overrides a member in the base class 'Car'.
Recommendation: enable the flag and always use the override keyword.
noImplicitReturns
When this flag is enabled, all possible code processing paths are verified for each function. If any of the paths does not return a declared type (or if the returned type is not declared, some paths return “something” and some do not) an error is thrown.
Example 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
}
When you enable the flag, for each of the above functions you will get an error:
Not all code paths return a value
The same error arises when you don’t explicitly define the returned type and let Typescript do it, but at the same time not all paths return any value.
Recommendation: always enable noImplicitReturns flag.
noImplicitThis
With the flag enabled, an error is dropped when you don’t define a type explicitly for “this”, and Typescript is unable to infer its type from the context.
Let’s take a look at an invallid 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;
}
}
}
What we have here is a method that contains a new defined function (a classic one, not an “arrow-function”, so the value of “this” changes depending on the context/way this function is called).
When you enable the flag, you will rightly receive an error:
'this' implicitly has type 'any' because it does not have a type annotation.
As a reminder, the “this” parameter can be explicitly typed, so that e.g. functions typed this way can be called only in a specific context. Let’s add the “strictBindCallApply” flag, which will allow us to freely change the types with restrictive verification.
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 noImplicitThis flag.
noPropertyAccessFromIndexSignature
If we type some of the fields with “index signature” then, without enabling the flag , we can refer to any fields with a dot (e.g. “foo.bar”), even if they are not 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'];
When the “noPropertyAccessFromIndexSignature” flag is enabled, attributes defined with “index signature” can only be accessed with “index signature“.
This ensures that using “dot” we will never refer to fields that may be undefined.
When the flag is enabled:
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 noPropertyAccessFromIndexSignature flag.
noUncheckedIndexedAccess
This flag works in conjunction with the “strictNullChecks” flag and causes the type “X | undefined” to be returned to fields that are typed with “index signature” as type “X”.
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();
When the flag is enabled:
Object is possibly 'undefined'.
Recommendation: always enable noUncheckedIndexedAccess flag.
noUnusedLocals
The principle is quite simple – unused declared local variables raise an error. Note that this also applies to unused modules imported into the file.
Example of a file with unused declared variables:
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: We encourage you to try it. Tools to remove unused imports (e.g. Ctrl+Alt+O in Webstorm by default) are very helpful.
noUnusedParameters
Like in the case of “noUnusedLocals”, declared but unused function arguments are forbidden.
// error: 'applicationName' is declared but its value is never read.
function sayHello(applicationName: string): void {
console.log(`Hello Foo`);
}
Recommendation: Always use this flag, because you get rid of unnecessary arguments, that among others clearly improves the code readability.
Restrictions on the compilation of Angular view templates
Let’s start with the fact that within the Angular view template compiler options, there are as many as 3 modes for verifying the types of variables used in the templates. They are presented as follows:
Basic:
In this mode we work with the following flag settings:
"angularCompilerOptions": {
"fullTemplateTypeCheck": false,
"strictTemplates": false,
...
}
When referencing variables, the only verification is whether those variables exist (are properties of the component class) and whether they have the referenced nested attributes.
For 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 following things are verified:
- whether “user” is a field in the component class,
- whether “user” is an object with the “address” field,
- whether “address” is an object with the “city” field
It is not verified if the type “user.address.city” is compatible with the input type “street” in the “app-child” component (it is not). The compilation will succeed, but an error will occur during the runtime:
Cannot read property 'length' of undefined
Additional things that are not verified at the compile stage in this mode:
- variables in “embedded” views (e.g. variables used in *ngIf, *ngFor, <ng-template> always have type “any”). The following example goes through the compilation process, the variables “fruit” and “user” have type “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 of references (#refs), values returned by pipes, types of $event values emitted by any outputs always have the type “any”.
Full mode:
In this mode we work with the following flag settings:
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictTemplates": false,
...
}
Compared to the basic mode, the following things change:
- variables in “embedded” views (e.g. variables inside *ngIf, *ngFor, <ng-template> blocks) have their type correctly detected and verified,
- the type of values returned from pipes is detected and verified,
- local references (#refs) to directives and pipes have their type correctly detected and verified (except when these parameters are generic),
In the example below, the local variable “fruit” is still of type “any”, but “user” is already typed correctly.
@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'
}
}
In this mode, an error will occur during compilation:
Property 'foo' does not exist on type '{ firstName: string; }'
Strict mode:
In this mode we work with the following flag settings:
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictTemplates": true,
...
}
Setting the “strictTemplates” flag to “true” always overrides the “fullTemplateTypeCheck” value (so the “fullTemplateTypeCheck” flag can be omitted in this case).
In this mode, the compiler offers us everything, the full verification mode has to offer, and moreover:
- for components and directives, it verifies the compatibility of input types with the variables assigned to them in the template (the strictNullChecks flag mentioned in the Typescript section is also taken into account during this verification),
- infer types for local variables inside the embedded views (e.g. a local variable declared inside the *ngFor structure directive),
- infer the $event value type for component outputs, DOM events and angular animations,
- infer the reference type (#refs) also for DOM elements based on the tag name (e.g. <span #spanRef> will be correctly 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 additionally enabled, we get the following error regarding the assignment of the “applicationName” value to the “name” input:
Type 'string | undefined' is not assignable to type 'string'
In this mode also the local variable “fruit” will have the type (string) correctly inferred, thus:
Property 'foo' does not exist on type 'string'.
When combining the strictNullChecks and strictTemplates flags, the often used async pipe is worth noting. The “transform” method type of this pipe is described as follows (using 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;
This means that for the following code:
@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 get an error:
Type 'string | null' is not assignable to type 'string'.
because the expression “applicationName$ | async” returns a value of the type “string | null”. This means that all inputs (to which the value is assigned using the async pipe) must be typed as nullable.
Note: the effect of the strictNullChecks flag on input type verification can be set using the strictNullInputTypes flag, which will be discussed later in this article.
Recommendation: We strongly discourage the use of the basic mode, we strongly encourage the use of the scrict mode, which will help avoiding many issues.
strictInputTypes
This flag is responsible for input type verification. If we do not set this flag manually, its default value matches the “strictTemplates” flag.
Concerning the value “true“, the types of variables assigned to the inputs are verified, as shown in the example for the restrictive verification mode, and if it is “false“, the verification is completely skipped (even if the strictTemplates flag is turned on at the same time).
As a reminder – the restrictiveness of this verification (i.e. taking nullable values into account) depends also on the value of strictNullChecks flag (and strictNullInputTypes flag, which will be discussed later in this article).
Recommendation: Always use this verification (either by setting this flag directly to true, or via strictTemplates flag).
strictInputAccessModifiers
This flag is responsible for verifying field access modifiers (private/protected/readonly) when assigning variables to inputs.
@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 {}
For the example above, the compilation will succeed without enabling the strictInputAccessModifiers flag, (no error will occur during the runtime). For the enabled flag, we get respectively:
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.
Applying the @Input decorator to a field with readonly/private/protected access modifiers seems like a programming error in any case (since it allows these values to be modified externally, at any time, which none of these modifiers theoretically allow), but it is only this extra flag that prevents such errors.
Recommendation: This flag is not included in strictTemplates, so you should enable it separately! We recommend using it (and not applying mentioned access modifiers to fields marked with the @Input decorator).
strictNullInputTypes
This flag determines whether the strictNullChecks flag is taken into account when verifying the types of variables assigned to Inputs. If you do not set this flag manually, its default value matches the value of the “strictTemplates” flag.
Let’s go back to the async pipe example again:
@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');
}
For the “strictNullChecks” flag enabled and the default values of the “strictInputTypes” and “strictNullInputTypes” flags (which could be omitted, in which case their value would be derived from the “strictTemplates” value)”
{
...
"compilerOptions": {
...
"strictNullChecks": true,
},
"angularCompilerOptions": {
"strictTemplates": true,
"strictInputTypes": true,
"strictNullInputTypes": true
}
}
we will get the same error as before (because the returned type from async pipe is nullable):
Type 'string | null' is not assignable to type 'string'.
However, if we set this flag to “false” while the “strictNullChecks“, “strictTemplates” and “strictInputTypes” flags are simultaneously enabled:
{
...
"compilerOptions": {
...
"strictNullChecks": true,
},
"angularCompilerOptions": {
"strictTemplates": true,
"strictInputTypes": true,
"strictNullInputTypes": false
}
}
then the compilation will succeed (because string matches string, and nullability is ignored).
Recommendation: We don’t recommend disabling this flag, although for existing projects (where inputs are not nullable) and for using libraries where components are not written to support nullable values it may be necessary.
strictAttributeTypes
This flag is responsible for verifying the assignment of values to inputs using “text attributes”. (instead of the classic bindings). If we don’t set this flag manually, its default value matches the value of the “strictTemplates” flag.
We usually bind attributes (including inputs for components and directives) with square brackets “[]”, informing the Angular compiler that the expression on the right is ‘dynamic’, so it contains an expression that needs to be evaluated ( in the simplest case this can be a variable reference).
There is also another way to set the value of an input – as a regular HTML attribute (remembering that all such attributes are strings). If the attribute name matches the input name, the input value is set right .
With the strictAttributeTypes flag disabled, the following example will pass compilation:
@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 will lead to the “weight” value being set to “18” (string, not number!), which will result in a runtime exception:
weight.toFixed is not a function
When the flag is enabled, the compiler will capture such an error:
Type 'string' is not assignable to type 'number'.
Assigning values to inputs without square brackets can be used only for Inputs that are typed as strings (and enums whose values are also strings).
Recommendation: enable this flag (when using strictTemplates, do not disable it).
strictSafeNavigationTypes
What are “safe navigations” operations? It is a well-known equivalent of Optional Chaining from Typescriptoccurring on the angular template side. For example:
@Component({
selector: 'app-root',
template: `
<p> {{ user?.address?.street }} </p>
`
})
export class AppComponent {
user: User = {
address: {
street: 'Sesame'
}
};
}
When this flag is disabled, any use of the safe navigation operator will cause its result to be treated as “any”. When this flag is enabled, the type will be correctly inferred. If you do not set this flag manually, its default value matches the value of the “strictTemplates” flag.
Without the safe navigation flag enabled, the operator only verifies whether “address” is a key in the “user” object, but it still treats that value as “any”, so it is possible to refer to non-existing fields (“bar.baz”).
@Component({
selector: 'app-root',
template: `
<p> {{ user?.address.bar.baz }} </p>
`
})
export class AppComponent {
user: User = {
address: {
street: 'Sesame'
}
};
}
We only learn about the error at runtime (“Cannot read property ‘baz’ of undefined”). If this flag is enabled, then the value returned by safe navigation operator is correctly inferred and the error will be detected already at compile time:
Property 'bar' does not exist on type '{ street: string; }'.
Recommendation: Use this flag (when using strictTemplates, do not disable it).
strictDomLocalRefTypes
This flag is responsible for disabling/enabling inference of angular reference types applied to DOM elements. If we do not set this flag manually, its default value matches the value of the “strictTemplates” flag. Our tests also show that without enabling the “strictTemplates” flag, reference types are not inferred regardless of the setting of the strictDomLocalRefTypes flag.
Example with strictDomLocalRefTypes flag disabled (with strictTemplates flag enabled):
@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 {}
Compilation passes, error appears in runtime (“Cannot read property ‘bar’ of undefined”).
With both flags enabled (the inferred reference type for the “input” tag is “HTMLInputElement”) a compile error occurs:
Property 'foo' does not exist on type 'HTMLInputElement'.
Recommendation: Use this flag (when using strictTemplates, do not disable it).
strictOutputEventTypes
This flag is responsible for disabling/enabling type inference for the `$event` value present in component/directive outputs and angular animations. If you do not set this flag manually, its default value matches the value of the “strictTemplates” flag.
Example with the flag disabled:
@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 {}
}
The compilation passes, the error appears in the runtime, after the first event is emitted (“Cannot read property ‘bar’ of undefined”).
With the flag enabled, the type $event is correctly inferred (as “number”) and a compilation error occurs:
Property 'foo' does not exist on type 'number'.
Recommendation: Use this flag (when using strictTemplates, do not disable it).
strictDomEventTypes
This flag, like the strictOutputEventTypes flag, is responsible for inferring the $event type, but this time for native DOM events. If we do not set this flag manually, its default value matches the value of the “strictTemplates” flag.
Example with the flag disabled:
@Component({
selector: 'app-root',
template: `<input type="text" (mouseenter)="$event.foo.bar">`
})
export class AppComponent {}
The compilation passes, the error appears in the runtime, after the first event is emitted (“Cannot read property ‘bar’ of undefined”).
With the flag enabled, the $event type is correctly inferred (as “MouseEvent”) and a compilation error occurs:
Property 'foo' does not exist on type 'MouseEvent'.
Recommendation: Use this flag (when using strictTemplates, do not disable it).
strictContextGenerics
This flag applies to generic types for components. If it is disabled, then any occurrence of a component’s generic type is interpreted as “any” during type inference in the angular template. When this flag is enabled, the component’s generic types are correctly resolved during type inference. If you do not set this flag manually, its default value matches the value of the “strictTemplates” flag.
Consider the following example:
@Component({
selector: 'app-child',
template: `
{{ value.length }} <!-- OK -->
{{ value.foo.bar.baz }} <!-- ERROR -->
`
})
export class ChildComponent<T extends Array<any>> {
@Input() value: T;
}
The variable “value” is of type “T”, so we know for sure that it is an array of some elements. However when the flag is disabled, “value” is interpreted on the template side as “any”, so we can easily get a runtime exception.
When the flag is enabled, we will get a compilation error:
Property 'foo' does not exist on type 'T'.
The compiler has no objections to using the ‘length’ field, because every array has this property.
Recommendation: Use this flag (do not turn it off when using strictTemplates).
strictLiteralTypes
This flag determines whether the variables (specifically objects and arrays) that we declare directly in the template have inherited type (if the flag is disabled, their value is “any”). If we do not set this flag manually, its default value matches the value of the “strictTemplates” flag.
Example with the disabled flag:
@Component({
selector: 'app-root',
template: `
{{ { firstName: 'foo '}.foo.bar.baz }}
{{ ['foo', 'bar'].foo.bar.baz }}
`
})
export class AppComponent {}
We declare two variables in the template (an object with the ‘firstName’ field and an array with two strings). Both are seen as ‘any’, so we can refer to non-existing fields (which will result in runtime errors).
With the enabled flag, we will get the following errors:
Property 'foo' does not exist on type '{ firstName: string; }'.
Property 'foo' does not exist on type 'string[]'.
Recommendation: Use this flag (when using strictTemplates, do not disable it).
Summary
Bravo! We went through the long list of possible settings. Now we know exactly what restrictions we can set to suit our needs. Each flag is (intentionally) presented as independently as possible of the others. However, this does not change the fact that all these restrictions coexist, they partly impact and complement each other.
Finding the perfect settings for your project (or for your team, because ultimately the configuration itself is reusable) can be a process full of testing and experimentation. In general, we suggest to always steering towards more restrictive configurations.
Automate it!
To tighten up the code verification process (including the compilability verification) we encourage you to add the project compilation stage to your continuous integration pipeline to (as mentioned at the very beginning of the article) catch any errors as soon as possible.
Restrictive compilation rules are a good addition to other code validation techniques (such as advanced static code analysis or any kind of automated tests). Each bug caught by the automated mechanism is ultimately a big time and money saver (compared to finding the same bugs during manual testing, or even worse, after the application is released to end-users).