In TypeScript, union types allow a variable to have one of several types. For example:
type Direction = 'North' | 'South' | 'East' | 'West';
Enums are a way to define a set of named constants.
enum Color {
Red,
Green,
Blue
}
Consider a function that takes a Direction
type and returns a message. If we don’t handle all possible directions, we may introduce bugs.
type Direction = 'North' | 'South' | 'East' | 'West';
function getDirectionMessage(direction: Direction): string {
if (direction === 'North') {
return 'You are going north';
} else if (direction === 'South') {
return 'You are going south';
}
// Bug: We didn't handle 'East' and 'West'
return 'Unknown direction';
}
Exhaustive checks ensure that all possible values of a union type or an enum are handled. TypeScript can enforce this at compile - time.
switch
StatementThe switch
statement is a common way to perform exhaustive checks in TypeScript.
type Direction = 'North' | 'South' | 'East' | 'West';
function getDirectionMessage(direction: Direction): string {
switch (direction) {
case 'North':
return 'You are going north';
case 'South':
return 'You are going south';
case 'East':
return 'You are going east';
case 'West':
return 'You are going west';
default:
// This will cause a compile - time error if all cases are not handled
const _exhaustiveCheck: never = direction;
return _exhaustiveCheck;
}
}
In the above code, the never
type is used in the default
case. If all possible values of the Direction
type are not handled in the switch
cases, the default
case will be reached, and TypeScript will raise a compile - time error because direction
cannot be assigned to the never
type.
type Shape = 'Circle' | 'Square' | 'Triangle';
function getShapeArea(shape: Shape): number {
if (shape === 'Circle') {
return 3.14;
} else if (shape === 'Square') {
return 4;
} else if (shape === 'Triangle') {
return 0.5;
}
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
When you update an enum or a union type, make sure to update all the code that performs exhaustive checks on them. For example, if you add a new direction to the Direction
type:
type Direction = 'North' | 'South' | 'East' | 'West' | 'NorthEast';
function getDirectionMessage(direction: Direction): string {
switch (direction) {
case 'North':
return 'You are going north';
case 'South':
return 'You are going south';
case 'East':
return 'You are going east';
case 'West':
return 'You are going west';
// Add a new case for 'NorthEast'
case 'NorthEast':
return 'You are going northeast';
default:
const _exhaustiveCheck: never = direction;
return _exhaustiveCheck;
}
}
Write unit tests to ensure that the exhaustive checks are working as expected. For example, you can test that adding a new value to a union type causes a compile - time error in the relevant functions.
Separate the code that performs exhaustive checks into small, reusable functions. This makes the code easier to maintain and test.
type Animal = 'Dog' | 'Cat' | 'Bird';
function getAnimalSound(animal: Animal): string {
switch (animal) {
case 'Dog':
return 'Woof';
case 'Cat':
return 'Meow';
case 'Bird':
return 'Chirp';
default:
const _exhaustiveCheck: never = animal;
return _exhaustiveCheck;
}
}
function printAnimalSound(animal: Animal) {
const sound = getAnimalSound(animal);
console.log(sound);
}
Use descriptive variable names like _exhaustiveCheck
in the default
case to make the code more readable and to clearly indicate the purpose of the code.
Exhaustive checks in TypeScript are a powerful tool for writing robust and error - free code. By ensuring that all possible values of a union type or an enum are handled, you can catch potential bugs at compile - time. Understanding the fundamental concepts, usage methods, common practices, and best practices will help you make the most of this feature in your TypeScript projects.
This blog post should give you a comprehensive understanding of exhaustive checks in TypeScript and how to use them effectively in your projects.