Bulletproof Angular. Angular strict mode explained

Angular uses TypeScript because TypeScript provides us with the tooling to create more robust applications. I'm talking about tools for type safety. But tons of developers aren't using the provided tools. They just create applications as they did 10 years ago using JavaScript.

Bulletproof Angular. Angular strict mode explained

Angular uses TypeScript because TypeScript provides us with the tooling to create more robust applications. I'm talking about tools for type safety. But tons of developers aren't using the provided tools. They just create applications as they did 10 years ago using JavaScript.

Also, Angular provides additional tools for checking our apps and those checks aren't enabled by default. Most developers have no idea about them.

All the cool tools for safe Angular applications will be enabled in new apps if you'll just add --strict=true flag when creating a new app through the Angular CLI.

In this article, I'm discussing the strict mode in Angular and how exactly it will help you build more robust Angular applications. Today we'll cover:

  • TypeScript strict mode
  • Avoid usage of any
  • No fallthrough cases in Switch blocks
  • Angular strict mode

TypeScript strict mode

Let's start with what I consider the most important tool - TypeScript strict mode. To enable it you specify strict: true in a tsconfig.json:

{
	"compilerOptions": {
		"strict": true
	}
}

This flag will enable the following options for the TypeScript compiler: noImplicitAny, noImplicitThis, strictNullChecks, strictPropertyInitialization, strictBindCallApply, strictFunctionTypes.

We'll not cover all of them, you can take a deeper look at this great article. But we'll cover three of the most important: strict null checks, noImplicitAny, and strictPropertyInitialization.

strictNullChecks

By default, it's okay to assign null or undefined to any value at TypeScript.

let someVariable: number = 666;

// Assigning null and everything is ok
foo = null;

// Assigning undefined and everything is ok
foo = undefined;

But in the strict mode, this code will throw an error since null and undefined aren't assignable to the type number. That's why, if you need to assign a null value to this variable, you ought to tell the TypeScript compiler that it can be a number or null.

let someVariable: number | null = 666;

// Assigning null and everything is ok
foo = null;

// Assigning undefined and IT'S AN ERROR!
foo = undefined;

Moreover, null and undefined are different for the TypeScript compiler! So, it's impossible to assign undefined to the variable that's declared as null. To fix that you ought to make the variable either null or undefined.

let someVariable: number | null | undefined = 666;

// Assigning null and everything is ok
foo = null;

// Assigning undefined and everything is ok
foo = undefined;

Did you know this? Looks pretty mind-blowing for me, However, this technique helps us track where and what values are assigned. TypeScript compiler with strictNullChecks flag will throw an error when you assign null or undefined to the variable without null | undefined in its type. Such behavior will make your code more robust and simpler to read. Since you’ll be sure no empty value is assigned to the variable without null | undefined in its type.

strictPropertyInitialization

This flag forces the author of the code to initialize all the class properties in the constructor. For example, this code will throw an error.

export class AppComponent {

	// No initializer here, tsc will throw an error
	title: string;
}

To fix it, you ought to initialize the title variable in place or at the constructor.

export class AppComponent {

	// In place initialization
	title: string = 'some text';
    
	// Initialization in constructor works to
	constructor() {
		this.title = 'some text';
	}
}

Also, if you don't want to initialize it by default in your class, you can shut the compiler up and tell him: 'Trust me, I know what I'm doing'. To do so, please, add an exclamation point like so.

export class AppComponent {

	title!: string;
}

This technique allows us to make sure that everything is initialized when a new object is created. So, you don’t need to check whether the prop is initialized or not. TypeScript will take care of this. You can be sure that every prop has data.

noImplicitAny

This one will force you to add types for all your variables (yep, we're almost turning Angular code into Java code). That's why the TypeScript compiler will throw an error during compilation for this function:

// No type declaration
function foo(bar): void {

	console.log(bar);
}

To make it work, you need to add a type to the bar:

function foo(bar: string): void {

	console.log(bar);
}

This will work. That's the most important rule that forces my teammates to hate me from time to time. Because they ought to add types in their apps! However, noImplicitAny guarantees that no one leaves a variable which type can’t be inference by the TypeScript compiler. Anyway, you can use any there:

function foo(bar: any): void {
	console.log(bar);
}

Not typesafe enough, right? For sure, but it’s a valid code from the TypeScrip compiler point of view. Here we have the only way - disallow usage of any (explained below).

As you can see above, adding a strict flag to your tsconfig.json will add you a few cool configurations and force you to:

  • Not use null in your apps when you don't know about that.
  • Always declare your variables and make sure they will have data when you'll access them.
  • Add types for your variables making them easily readable and more robust.

That's all I want to tell you about the strict mode. Now let's dive into one of the funniest rules I'm introducing in this article.

Not using Any

Yep, we don’t want to use any. This rule forces you to NOT use type any in your project. You can enable it via adding no-any: true in your tslint.json.

{
	"rules": {
		"no-any": true
	}
}

Creating applications without any makes your apps more robust. If your app uses no-any, you can make sure that every type in the app will be defined and each developer will understand what each variable in the codebase means.

Also, the TypeScript compiler will bark on you any time you're not defining the type of the variable and you'll have to do that. The usage of the no-any with strictPropertyInitialization, noImplicitAny, and strict null checks will make your code bulletproof. So, don't hesitate, start your project with them.

noFallthroughCasesInSwitch

This one is great. I like it. It forces you to not use fall-through case statements. That's why this will throw an error.

const foo: number = 0;

switch (foo) {

	case 0: // The error will be thrown here
		console.log('0');
        
	case 1: // No error here
		console.log('1');
		break;
}

Because you have no break statement after the first console.log that's considered a bad practice. Please, don't use the switch/case that way. What's acceptable? Using break statements is ok as you can see above. Also, it's acceptable to use empty fall-through case statements.

const foo: number = 0;

switch (foo) {
	case 0: // No error here
	case 1: // No error here
		console.log('1');
		break;
}

The code above is much better. Why? Because if you have a fall through case statement you’re preparing the ground for tons of unexpected errors. Multiple case statements will be executed all the time. Other people will never expect that by default. While at the fixed version the code flow is predefined and pretty simple.

That's all about cool TypeSctrict checks I wanted to tell you in this article. Now, let's go higher - talk about Angular strict checks

Angular strict mode

Angular does a few checks in the template by default but they can't guarantee that your app will work properly. Enabling Angular strict mode through the Angular CLI will enable TypeScript strict mode in your app also as enable some Angular strict checks. Angular Strict template checking is enabled via strictTemplates in tsconfig.json. It'll force the Angular compiler to check tons of things.

Proper data assignment to component’s inputs

Assuming we have the following code:

@Component({
	selector: 'app-child',
	template: ' {{ title }}',
})
export class ChildComponent {
	@Input() title: string = '';
}

@Component({
	selector: 'app-root',
	template: `
		<app-child [title]="12345678"></app-child>
	`,
})
export class AppComponent {}

Focus on the title input of the ChildComponent. It has a string type. While we’re trying to push a number inside. Angular compiler detects it and throws an error. Plain type error.

Pipes have the correct return type

Let’s get back to the code, but add a pipe there:

@Pipe({ name:'testPipe'})
export class TestPipe  implements PipeTransform {
	transform(value: any, ...args: any[]): number {
		return 0;
	}
}

@Component({
	selector: 'app-child',
	template: ' {{ title }}',
})
export class ChildComponent {
	@Input() title: string = '';
}

@Component({
	selector: 'app-root',
	template: `
		<app-child [title]="'Hey! Im a title' | testPipe"></app-child>
	`,
})
export class AppComponent {}

Again, we have a child component with title input. It has a type string and we’re passing the string inside. However, we’re passing it through the testPipe that has a number return pipe. And here is the problem. The resulting type of the 'Hey! I’m a title' | testPipe is a number. While ChildComponent#title receives only strings. Such behavior will throw an error.

The correct type of event$'s

This check guarantees that every event will have a proper type in the template.

@Component({ selector: 'app-child' })
export class ChildComponent {
	@Output() titleChange: EventEmitter<string> = new EventEmitter();
}

@Component({
	selector: 'app-root',
	template: `
		<app-child (titleChange)="update($event)"></app-child>
	`,
})
export class AppComponent {
	update(title: number): void {}
}

Here we have almost the same code as before. While now we're working with the titleChange event. That has a string type. However, I’m trying to assign an update(title: number) callback to it that is unsuitable here because of the number type instead of string.

So, I would say strict template checking makes your app more robust and easier to support. To get a detailed explanation of the strict template checking, please, refer to this doc.

Do I need a strict mode?

Angular provides us with tons of flags that allow configuring what strict features will be turned on. But most of the important flags will be turned on if you'll create an app through the ng new my-app --strict=true command. Strict compilation mode will force you to take types into account, use nulls, and undefined properly. That's why I would say that strict mode is a must for your app.

BUT. it's cool until it's not. From time to time strict mode gives you surprises. For instance, how do you feel this code will work with Angular strict mode:

@Component({
	selector: 'app-child',
	template: `{{ title }}`,
})
export class ChildComponent {
	@Input() title: string = '';
}

@Component({
	selector: 'app-root',
	template: `
		<app-child [title]="title$ | async"></app-child>
	`,
})
export class AppComponent {
	title$: Observable<string> = of(`I'm a title`);
}

That's a pretty common approach - use a stream and pass it inside the @Input using the async pipe. However, if you dive inside the async pipe source code you'll see that it will pass null first of all inside the input, and only when title$ fires it'll pass that value inside the input. That's why the type of the title$ is Observable<string> but the type of the title$ | async is Observable<string | null>;

BUT 2.

Don't turn it on in the project that's already running in the production. Don't turn it on for the team that used to build projects without strict mode - strict mode will freak your teammates out. They will hate you. I made that mistake once. And as you can guess it didn’t go well.

Thank you for the reading! Follow me on Twitter to stay connected!

Resources