Angular Opting in to Strict Mode
Angular v10 strict opt-in mode that allows to perform more build-time optimizations and help you deliver faster apps with fewer defects. This mode is still only an opt-in because it comes with its trade-offs — stricter type checking and extra configuration.
ng new my-app --strict
The command above will generate a workspace with the following settings enabled on top of the defaults:
Researchers have proven empirically that TypeScript’s compiler helps us fix more issues before we ship our app to production. For more details, check “To Type or Not to Type: Quantifying Detectable Bugs in JavaScript.” To not step on the way of folks getting started with Angular, by default, we currently don’t enable the strictest TypeScript compiler options in new CLI workspaces.
One of the first things many developers using Angular do, to get more help from the compiler is to enable the following flags in tsconfig.json:
This way, they can more confidently ship to production because the compiler has given them some guarantees that their app aligns to a specific contract, defined by the type system. In the CLI’s strict mode, we enable the TypeScript’s strict flag, which turns these on by default.
Two of the flags that will have the biggest impact on your development process are strictPropertyInitialization and strictNullChecks. With strictPropertyInitialization, the following snippet will throw a compile-time error because we haven’t initialized title:
@Component({...})
class AppComponent {
@Input() title: string;
}
To fix it, you’d have to either set title to a string value or change its type, for instance:
@Component({...})
class AppComponent {
@Input() title = '';
}
One additional check we’ve enabled is the no-any TSLint rule (yes, we’ll be moving away from TSLint; this rule has its ESLint equivalent). This way, we can use more type information during our ng update command and more confidently migrate your apps across Angular versions. Let us look at an example:
import { HttpClient } from '@angular/common/http';
@Component({...})
export class AppComponent {
client: any;
constructor(http: HttpClient) {
this.client = http;
}
handleClick() {
this.client.deprecatedMethod();
}
}
If we wanted to migrate your project away from deprecatedMethod we wouldn’t have been able to given that the type of client is any. To migrate your project with confidence, we’d need the information that client has the type of HttpClient with an explicit type annotation: client: HttpClient. This way we’d know that deprecatedMethod belongs to our migration target rather than it just happened that another object has a method with the same name.
The strictTemplates flag is an Angular specific configuration that strict mode enables under angularCompilerOptions in tsconfig.json. Let’s look at the following snippet:
<div *ngFor="let todo of todos">
<h2>{{ todo.title }}</h2>
<button *ngIf="user.isAdmin">Edit</button>
</div>
Currently, Angular will not perform any type checking at compile-time. If you enable strictTemplates, Angular will check if todo has a property title and if user.isAdmin exists. Additionally, if you have any of the strict TypeScript flags enabled, the Angular compiler will enable the same strictness checks for the template. For example, if we have strictNullChecks and strictTemplates in the following snippet we’ll get a type error:
@Component({
template: '<h1>{{ article.title }}</h1>'
})
class ArticleComponent {
article: Article | undefined;
}
This will help us ensure we’ve initialized article before accessing its property. A way to fix this error would be: <h1>{{ article?.title }}</h1> using the safe navigation operator (similar to optional chaining).
[Pro] strictNullChecks and strictPropertyInitialization will make sure you’re not accessing properties or calling methods of null references.
[Pro] Angular will also be able to help you discover more issues ahead of time by type checking your templates.
Bundle budgets provide a guarantee that the performance of your app will not unnoticeably regress over time. In Angular CLI’s angular.json file, we have predefined error and warning budgets. If the size of a specific JavaScript bundle or style exceeds the warning budget, you’ll get a warning message; if you exceed the error budget, your build will fail.
By default, we’ve set pretty high budgets:
In strict mode, these numbers drop to:
To make sure you fit the budget:
[Con] According to reports we have, over 50% of apps ship more than 1MB of JavaScript. With the tighter bundle budgets, your build will fail if you exceed this number. If the initial load time performance of your app is not critical for your use case, you’ll be getting build failures and you will have to update the budget manually.
[Pro] If you introduce a large dependency by accident, your build will fail, and you’ll catch the regression ahead of time. Overall, bundle budgets will guard your application against growing in size.
It’s practically impossible to determine with static code analysis if a module produces side effects or not in a language as dynamic as JavaScript. This means that our optimistic assumption may potentially break your app. Let us look into more details what this actually means.
Currently, with the Angular CLI webpack delegates tree-shaking to terser, the JavaScript optimizer we use. Let’s look at an example:
// constants.js
export const PI = 3.14159265357989;
// index.js
import { add } from './utils';
const subtract = (a, b) => a - b;
console.log(add(1, 2));
// operations.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// utils.js
export * from './operations';
export * from './constants';
Here we have the following dependency graph:
When we run webpack with entry point index.js we’ll get the following output:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./operations.js
// utils.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./constants.js
const PI = 3.14159265357989;
// CONCATENATED MODULE: ./utils.js
// CONCATENATED MODULE: ./index.js
// index.js
const index_subtract = (a, b) => a - b;
console.log(add(1, 2));
/******/ })();
Notice that although we don’t use subtract and PI, they are still in the final bundle. If we run webpack in production mode, terser will statistically determine that we’re only using add, and it will get rid of the exported subtract and PI.
webpack includes the module constants.js in the final bundle only because it’s not sure if it produces side effects or not. For example, if it adds globals, writes to localStorage, or sends HTTP requests, removing this module will break our app.
Suppose we specify the sideEffects property to false in the corresponding package.json to our project. In that case, webpack will assume utils.js, constants.js, and operations.js do not produce any side effects and will not include the unused modules in the final bundle. Here’s what the final bundle would look like in this case:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./utils.js
// utils.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js
// index.js
const index_subtract = (a, b) => a - b;
console.log(add(1, 2));
/******/ })();
If we enable terser, it’ll get rid of index_subtract and subtract since we don’t use them, and inline add, which will produce:
(()=>{"use strict";console.log(1+2)})();
[Con] If you have side effects in any of your modules and webpack tree-shakes it away, you’ll get a runtime error.
[Pro] You’ll get smaller apps overall.
Let us spend a minute to look at the second point. Imagine constants.js had the following content:
// constants.js
export const PI = 3.14159265357989;
localStorage.setItem('foo', 'bar');
If we have incorrectly specified the sideEffects flag to false and webpack gets rid of this module since we don’t reference it, the localStorage.setItem('foo', 'bar') statement will not execute. This, later on, can lead to issues in our app if we rely that we have this information in localStorage.
Everything we mentioned so far has its trade-offs. If we want fewer issues in production, we need to fight with stricter compiler options, and if we want smaller bundles, we may have to enable extra, potentially unsafe configuration.
Opting in to Strict Mode
To opt into the strict mode, you need to create a new Angular CLI app, specifying the --strict flag:ng new my-app --strict
The command above will generate a workspace with the following settings enabled on top of the defaults:
- Strict mode in TypeScript, as well as other strictness flags recommended by the TypeScript team. Specifically, strict, forceConsistentCasingInFileNames, noImplicitReturns, noFallthroughCasesInSwitch
- Turns on strict Angular compiler flags strictTemplates and strictInjectionParameters
- Reduced bundle size budgets by ~75%
- Turns on no-any TSLint rule to prevent declarations of type any
- Marks your application as side-effect free to enable more advanced tree-shaking
Stricter Type Checking
Researchers have proven empirically that TypeScript’s compiler helps us fix more issues before we ship our app to production. For more details, check “To Type or Not to Type: Quantifying Detectable Bugs in JavaScript.” To not step on the way of folks getting started with Angular, by default, we currently don’t enable the strictest TypeScript compiler options in new CLI workspaces.
One of the first things many developers using Angular do, to get more help from the compiler is to enable the following flags in tsconfig.json:
- strictPropertyInitialization
- strictNullChecks
- noImplicitAny
- strictBindCallApply
- strictFunctionTypes
This way, they can more confidently ship to production because the compiler has given them some guarantees that their app aligns to a specific contract, defined by the type system. In the CLI’s strict mode, we enable the TypeScript’s strict flag, which turns these on by default.
Two of the flags that will have the biggest impact on your development process are strictPropertyInitialization and strictNullChecks. With strictPropertyInitialization, the following snippet will throw a compile-time error because we haven’t initialized title:
@Component({...})
class AppComponent {
@Input() title: string;
}
To fix it, you’d have to either set title to a string value or change its type, for instance:
@Component({...})
class AppComponent {
@Input() title = '';
}
One additional check we’ve enabled is the no-any TSLint rule (yes, we’ll be moving away from TSLint; this rule has its ESLint equivalent). This way, we can use more type information during our ng update command and more confidently migrate your apps across Angular versions. Let us look at an example:
import { HttpClient } from '@angular/common/http';
@Component({...})
export class AppComponent {
client: any;
constructor(http: HttpClient) {
this.client = http;
}
handleClick() {
this.client.deprecatedMethod();
}
}
If we wanted to migrate your project away from deprecatedMethod we wouldn’t have been able to given that the type of client is any. To migrate your project with confidence, we’d need the information that client has the type of HttpClient with an explicit type annotation: client: HttpClient. This way we’d know that deprecatedMethod belongs to our migration target rather than it just happened that another object has a method with the same name.
The strictTemplates flag is an Angular specific configuration that strict mode enables under angularCompilerOptions in tsconfig.json. Let’s look at the following snippet:
<div *ngFor="let todo of todos">
<h2>{{ todo.title }}</h2>
<button *ngIf="user.isAdmin">Edit</button>
</div>
Currently, Angular will not perform any type checking at compile-time. If you enable strictTemplates, Angular will check if todo has a property title and if user.isAdmin exists. Additionally, if you have any of the strict TypeScript flags enabled, the Angular compiler will enable the same strictness checks for the template. For example, if we have strictNullChecks and strictTemplates in the following snippet we’ll get a type error:
@Component({
template: '<h1>{{ article.title }}</h1>'
})
class ArticleComponent {
article: Article | undefined;
}
This will help us ensure we’ve initialized article before accessing its property. A way to fix this error would be: <h1>{{ article?.title }}</h1> using the safe navigation operator (similar to optional chaining).
Trade-offs
[Con] The compiler will be stricter. It will throw compile-time errors if you haven’t initialized a property, check if an expression is null, if you have type errors in your templates, etc. If you want to prototype something rapidly and correctness is not important to you, this may slow you down. Such compile-time errors could also be confusing for people getting started with Angular.[Pro] strictNullChecks and strictPropertyInitialization will make sure you’re not accessing properties or calling methods of null references.
[Pro] Angular will also be able to help you discover more issues ahead of time by type checking your templates.
Tighter Bundle Budgets
Bundle budgets provide a guarantee that the performance of your app will not unnoticeably regress over time. In Angular CLI’s angular.json file, we have predefined error and warning budgets. If the size of a specific JavaScript bundle or style exceeds the warning budget, you’ll get a warning message; if you exceed the error budget, your build will fail.
By default, we’ve set pretty high budgets:
- 2MB warning and 5MB error budget for the initial JavaScript we send to the browser
- 6KB warning and 10KB error budget for the styles of any component
In strict mode, these numbers drop to:
- 500KB warning and 1MB error budget for the initial JavaScript we send to the browser
- 2KB warning and 4KB error budget for the styles of any component
To make sure you fit the budget:
- Make sure you are aware of what you have in your production bundles. source-map-explorer will come handy here
- Use lazy-loading
- Avoid large imports in your component styles
Trade-offs
[Con] According to reports we have, over 50% of apps ship more than 1MB of JavaScript. With the tighter bundle budgets, your build will fail if you exceed this number. If the initial load time performance of your app is not critical for your use case, you’ll be getting build failures and you will have to update the budget manually.[Pro] If you introduce a large dependency by accident, your build will fail, and you’ll catch the regression ahead of time. Overall, bundle budgets will guard your application against growing in size.
Reduced side effects
The last strict mode option we will look at is related to tree-shaking. Tree-shaking is a form of dead code elimination, in which the build tooling removes unused code. To enable webpack to remove unused modules, in the strict mode, we create an extra package.json with all of your apps and libraries. This package.json file has a single property - sideEffects set to false.It’s practically impossible to determine with static code analysis if a module produces side effects or not in a language as dynamic as JavaScript. This means that our optimistic assumption may potentially break your app. Let us look into more details what this actually means.
Currently, with the Angular CLI webpack delegates tree-shaking to terser, the JavaScript optimizer we use. Let’s look at an example:
// constants.js
export const PI = 3.14159265357989;
// index.js
import { add } from './utils';
const subtract = (a, b) => a - b;
console.log(add(1, 2));
// operations.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// utils.js
export * from './operations';
export * from './constants';
Here we have the following dependency graph:
When we run webpack with entry point index.js we’ll get the following output:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./operations.js
// utils.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./constants.js
const PI = 3.14159265357989;
// CONCATENATED MODULE: ./utils.js
// CONCATENATED MODULE: ./index.js
// index.js
const index_subtract = (a, b) => a - b;
console.log(add(1, 2));
/******/ })();
Notice that although we don’t use subtract and PI, they are still in the final bundle. If we run webpack in production mode, terser will statistically determine that we’re only using add, and it will get rid of the exported subtract and PI.
webpack includes the module constants.js in the final bundle only because it’s not sure if it produces side effects or not. For example, if it adds globals, writes to localStorage, or sends HTTP requests, removing this module will break our app.
Suppose we specify the sideEffects property to false in the corresponding package.json to our project. In that case, webpack will assume utils.js, constants.js, and operations.js do not produce any side effects and will not include the unused modules in the final bundle. Here’s what the final bundle would look like in this case:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./utils.js
// utils.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js
// index.js
const index_subtract = (a, b) => a - b;
console.log(add(1, 2));
/******/ })();
If we enable terser, it’ll get rid of index_subtract and subtract since we don’t use them, and inline add, which will produce:
(()=>{"use strict";console.log(1+2)})();
Trade-offs
[Con] You now have multiple package.json files — one for your workspace, and one for each of your libraries and apps. This could be confusing for folks getting started with Angular CLI.[Con] If you have side effects in any of your modules and webpack tree-shakes it away, you’ll get a runtime error.
[Pro] You’ll get smaller apps overall.
Let us spend a minute to look at the second point. Imagine constants.js had the following content:
// constants.js
export const PI = 3.14159265357989;
localStorage.setItem('foo', 'bar');
If we have incorrectly specified the sideEffects flag to false and webpack gets rid of this module since we don’t reference it, the localStorage.setItem('foo', 'bar') statement will not execute. This, later on, can lead to issues in our app if we rely that we have this information in localStorage.
Everything we mentioned so far has its trade-offs. If we want fewer issues in production, we need to fight with stricter compiler options, and if we want smaller bundles, we may have to enable extra, potentially unsafe configuration.
Comments
Post a Comment