TypeScript's strict null checks feature is designed to eliminate null and undefined errors, which are common sources of bugs in JavaScript. By enabling strict null checks, TypeScript forces you to handle null and undefined values explicitly, making your code more robust and reliable. This chapter will explain strict null checks in detail, covering the basics, advanced usage, and best practices with plenty of examples.
In JavaScript, null and undefined are two distinct types:
null is an assignment value that represents the absence of any object value.undefined indicates that a variable has been declared but not assigned a value.
let a = null;
let b;
console.log(a); // null
console.log(b); // undefined
a is explicitly set to null.b is declared but not initialized, so it is undefined.Without strict null checks, functions and variables might unexpectedly be null or undefined, leading to runtime errors.
function getLength(str: string) {
return str.length;
}
const result = getLength(null); // Runtime error
getLength(null) results in a runtime error because null has no length property.To enable strict null checks, you need to set the strictNullChecks option to true in your tsconfig.json file.
{
"compilerOptions": {
"strictNullChecks": true
}
}
function getLength(str: string | null): number {
if (str === null) {
return 0;
}
return str.length;
}
const result = getLength(null); // 0
str parameter is a union type (string | null), explicitly allowing null.str is null and handles it accordingly.
0
Type guards are a way to narrow down the type within a conditional block.
function printLength(value: string | number | null) {
if (typeof value === 'string') {
console.log(value.length);
} else if (typeof value === 'number') {
console.log(value.toString().length);
} else {
console.log('Value is null');
}
}
printLength('Hello'); // 5
printLength(12345); // 5
printLength(null); // Value is null
typeof operator checks the type of value, and TypeScript narrows down the type within each conditional block.
5
5
Value is null
You can create custom type guards to handle complex types.
interface Cat {
name: string;
purrs: boolean;
}
interface Dog {
name: string;
barks: boolean;
}
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).purrs !== undefined;
}
function greetAnimal(animal: Cat | Dog) {
if (isCat(animal)) {
console.log(`Hello, ${animal.name}. Do you purr?`);
} else {
console.log(`Hello, ${animal.name}. Do you bark?`);
}
}
greetAnimal({ name: 'Whiskers', purrs: true }); // Hello, Whiskers. Do you purr?
greetAnimal({ name: 'Rover', barks: true }); // Hello, Rover. Do you bark?
isCat is a user-defined type guard that checks if animal is a Cat.if block.
Hello, Whiskers. Do you purr?
Hello, Rover. Do you bark?
TypeScript provides the NonNullable utility type to exclude null and undefined from a type.
type NonNullableString = NonNullable;
function printNonNullable(str: NonNullableString) {
console.log(str);
}
printNonNullable('Hello'); // Hello
// printNonNullable(null); // Error: Argument of type 'null' is not assignable
NonNullable<string | null | undefined> results in string.null nor undefined.
Hello
Hello
function getFirstChar(str: string | null): string {
return str!.charAt(0);
}
console.log(getFirstChar('TypeScript')); // T
// console.log(getFirstChar(null)); // Runtime error
! operator is used to assert that str is non-null.null is not handled properly, so use it cautiously.
T
Interfaces can have optional properties, which might be undefined.
interface User {
id: number;
name: string;
email?: string;
}
function printUser(user: User) {
console.log(`ID: ${user.id}, Name: ${user.name}`);
if (user.email) {
console.log(`Email: ${user.email}`);
}
}
const user1: User = { id: 1, name: 'Alice' };
const user2: User = { id: 2, name: 'Bob', email: 'bob@example.com' };
printUser(user1);
printUser(user2);
email is an optional property.email is present before trying to access it.
ID: 1, Name: Alice
ID: 2, Name: Bob
Email: bob@example.com
Optional chaining (?.) simplifies accessing optional properties.
function printUserWithOptionalChaining(user: User) {
console.log(`ID: ${user.id}, Name: ${user.name}`);
console.log(`Email: ${user.email?.toLowerCase() ?? 'No email'}`);
}
printUserWithOptionalChaining(user1);
printUserWithOptionalChaining(user2);
user.email?.toLowerCase() safely accesses email and calls toLowerCase() if email is not undefined.?? operator provides a default value if email is undefined.
ID: 1, Name: Alice
Email: No email
ID: 2, Name: Bob
Email: bob@example.com
Always initialize variables to avoid them being undefined.
let name: string = 'John';
let age: number = 30;
console.log(name, age);
undefined. Use default parameters to handle undefined arguments.
function greet(name: string = 'Guest') {
console.log(`Hello, ${name}`);
}
greet(); // Hello, Guest
greet('Alice'); // Hello, Alice
name defaults to 'Guest' if no argument is provided.
Hello, Guest
Hello, Alice
(!), as it bypasses TypeScript’s null checks and can lead to runtime errors.
function printLengthWithNonNullAssertion(str: string | null) {
console.log(str!.length);
}
printLengthWithNonNullAssertion('Hello'); // 5
// printLengthWithNonNullAssertion(null); // Runtime error
! operator asserts that str is non-null, which can cause errors if str is actually null.
5
Running printLengthWithNonNullAssertion(null) will result in a runtime error since null is not handled.
Use type guards and assertions to safely handle null and undefined values.
function assertIsDefined(value: T): asserts value is NonNullable {
if (value === undefined || value === null) {
throw new Error('Value must be defined');
}
}
function printLengthWithAssertion(str: string | null) {
assertIsDefined(str);
console.log(str.length);
}
printLengthWithAssertion('Hello'); // 5
// printLengthWithAssertion(null); // Error: Value must be defined
assertIsDefined is a custom assertion function that throws an error if the value is null or undefined.str is non-null before accessing its properties.
5
printLengthWithAssertion(null) will throw an error, preventing a runtime error later.Strict null checks in TypeScript enhance code reliability by enforcing explicit handling of null and undefined values. By understanding and implementing these checks, you can avoid common pitfalls associated with nullable values and write safer, more maintainable code. This chapter covered the fundamentals of strict null checks, type guards, utility types, and best practices with practical examples. Happy coding !❤️
