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 !❤️