TypeScript's strict mode
Let's begin with what I'd argue is the most valuable tool in the entire toolbox: TypeScript strict mode. You activate it by adding strict: true to your tsconfig.json:
{
"compilerOptions": {
"strict": true
}
}
Flipping this single switch enables a suite of compiler options: noImplicitAny, noImplicitThis, strictNullChecks, strictPropertyInitialization, strictBindCallApply, and strictFunctionTypes.
We won't go through every one of them – this excellent article covers the full list. Instead, let's zero in on the three that matter most: strict null checks, noImplicitAny, and strictPropertyInitialization.
strictNullChecks
Out of the box, TypeScript happily lets you assign null or undefined to any variable.
let someVariable: number = 666;
// Assigning null and everything is ok
foo = null;
// Assigning undefined and everything is ok
foo = undefined;
Turn on strict mode, and that same code becomes a compile error — null and undefined simply aren't assignable to the number type. If you genuinely need to accept a null value, you have to tell the compiler explicitly that the variable can be either 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;
Here's a fascinating twist: the TypeScript compiler treats null and undefined as distinct types! Assigning undefined to a variable declared as null is impossible. The fix is to declare the variable as accepting either of them.
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 about that distinction? It certainly surprised me at first. But this behavior gives us precise tracking of what values go where. With strictNullChecks active, the compiler rejects any assignment of null or undefined to a variable whose type doesn't include null | undefined. The payoff is code that's more reliable and easier to read — you're never left wondering if an empty value slipped in where it shouldn't be.
strictPropertyInitialization
This flag demands that every class property be initialized within the constructor. The code below will trigger an error.
export class AppComponent {
// No initializer here, tsc will throw an error
title: string;
}
You can remedy it by initializing the title variable either at the point of declaration or inside the constructor.
export class AppComponent {
// In place initialization
title: string = 'some text';
// Initialization in constructor works to
constructor() {
this.title = 'some text';
}
}
If you prefer to defer initialization, you can silence the compiler with a definite assignment assertion — just append an exclamation mark to tell TypeScript: "Trust me, I know what I'm doing."
export class AppComponent {
title!: string;
}
This technique guarantees that every property holds data the moment an object is created. There's no need to manually check whether a property has been initialized — TypeScript does that verification for you, giving you confidence that every property is populated.
noImplicitAny
This option forces you to give explicit types to every variable — yes, Angular code starts to feel a bit like Java. Consider this function; the TypeScript compiler will refuse to compile it.
// No type declaration
function foo(bar): void {
console.log(bar);
}
To satisfy the compiler, you simply add a type to the bar parameter:
function foo(bar: string): void {
console.log(bar);
}
That compiles fine. This is the rule that occasionally earns me some grumbles from my teammates, because they have to write types everywhere. But noImplicitAny guarantees that no variable slips through without an explicit type when the compiler can't infer one. You could still fall back to any if you wanted to:
function foo(bar: any): void {
console.log(bar);
}
Not exactly type-safe, is it? It's technically valid TypeScript, but it defeats the purpose. That's why we also need the separate rule banning any outright (covered in a moment).
Adding strict to your tsconfig.json brings a bunch of useful restrictions that push you toward:
- Avoiding null in places where you haven't accounted for it.
- Declaring all your variables upfront, ensuring they hold data when accessed.
- Providing explicit types that make your code far more readable and maintainable.
That wraps up the core TypeScript strict mode options. Now let's look at one of the most enjoyable rules to enforce in a codebase.
Banning any
We absolutely don't want any in our code. This rule forbids the use of the any type throughout your project, enabled simply by adding no-any: true to your tslint.json.
{
"rules": {
"no-any": true
}
}
Eliminating any makes your application significantly more robust. Every type in the codebase is explicitly defined, so each developer instantly understand's what each variable represents. The TypeScript compiler will call out any place where you've failed to specify a type, essentially forcing you to define it. Combining no-any with strictPropertyInitialization, noImplicitAny, and strictNullChecks results in remarkably bulletproof code. It's well worth starting your next project with these settings enabled.
noFallthroughCasesInSwitch
This one is a personal favorite. It prevents the use of fall-through behavior in switch statements, so the following code would trigger 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 the first console.log doesn't end with a break, it falls straight into the second case — and that's considered poor practice. Avoid writing switch/case blocks this way. Proper break statements are completely acceptable, and an empty fall-through is fine too.
const foo: number = 0;
switch (foo) {
case 0: // No error here
case 1: // No error here
console.log('1');
break;
}
The revised version is far superior. Why? Fall-through cases are a breeding ground for subtle, unexpected bugs. Multiple case blocks will execute every time, and nobody reading the code will expect that behavior. The corrected version has a clear, predictable flow of execution.
That's everything I wanted to share about the typeScript-side checks. Now let's move up a level and talk about Angular's own strict checks.
Angular strict mode
Angular performs some template validation out of the box, but it's not enough to guarantee your app behaves correctly. Enabling Angular strict mode through the Angular CLI activates TypeScript strict mode in your project as well as turning on several Angular-specific checks. The key flag is strictTemplates in tsconfig.json, which directs the Angular compiler to validate a much broader range of things in your templates.
Input types must match
Suppose we have the following component setup:
@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 {}
Look at the title input on ChildComponent. It's declared as a string, yet we're passing a number to it. The Angular compiler catches this mismatch right away and raises an error. Straightforward type checking in the template.
Pipe return types are validated
Let's revisit the same setup, this time adding a pipe into the mix:
@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 a title input that expects a string type, and we're passing a string literal through the testPipe. However, that pipe returns a number, not a string. The resulting type of 'Hey! I'm a title' | testPipe is therefore a number, while ChildComponent#title only accepts strings. The compiler detects this and throws an error.
Event handler types are strictly checked
This check guarantees that event handlers in the template receive correctly typed event payloads.
@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 code similar to the previous example, but this time we're dealing with the titleChange event output, which emits a string. I'm attempting to wire it to an update(title: number) callback that expects a number — a definite mismatch that the compiler immediately flags.
Strict template checking genuinely improves code robustness and makes long-term maintenance less painful. For a comprehensive rundown of all its capabilities, the official template type checking documentation is an excellent reference.
Is strict mode necessary?
Angular exposes numerous flags that fine-tune which strict features are enabled. You get the most important ones automatically by creating a new app with ng new my-app --strict=true. The strict compilation mode forces you to respect types and handle null and undefined correctly. Given these benefits, I'd confidently say that strict mode is a must-have for any Angular application.
There is a caveat, though: it's great right up until it isn't. Strict mode can spring surprises on you from time to time. Consider how this code behaves under 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`);
}
This pattern is ubiquitous — use a stream and pass it to an @Input via the async pipe. However, peek into the implementation of the async pipe and you'll discover it passes null into the input first, and only later emits the actual value from title$. Consequently, while title$ is typed as Observable<string>, the resulting type of title$ | async becomes Observable<string | null>, which requires careful handling.
One more important warning: don't enable strict mode on a project that's already in production. And don't enable it for a team that's grown comfortable without it — the flood of new errors will frustrate everyone and could turn them against the change. I made that mistake once, and it did not go over well.
Thanks for reading! Follow me on Twitter to stay updated!
