Skip to main content

Conditionals

Let's learn the best practices for writing conditionals. Leveraging the TypeScript type system lets you write safer, more readable branches.

Key points

  • Make conditions easy to understand
  • Reduce the reader's burden
  • Use early returns
  • Don't nest deeply
  • Consider techniques other than if statements

Don't write the same branch in many places

When the same conditional branch is scattered across multiple places, the scope of impact for a spec change grows. Keep branches in one place.

Bad

❌ Bad: The same branch is scattered
// ❌ The same branch is scattered
type SkillName = 'Tackle' | 'Quick Attack' | 'Iron Tail' | 'Thunderbolt' | 'Thunder';

function getAttackPoint(skill: SkillName): number {
switch (skill) {
case 'Iron Tail':
return 30;
case 'Thunderbolt':
return 100;
case 'Thunder':
return 1000;
default:
return 0;
}
}

function getHp(skill: SkillName, currentHp: number): number {
let attackPoint: number;
if (skill === 'Tackle') {
attackPoint = 10;
} else if (skill === 'Quick Attack') {
attackPoint = 20;
} else {
attackPoint = getAttackPoint(skill);
}
return currentHp - attackPoint;
}

Good

✅ Good: Keep the branch in one place
// ✅ Keep the branch in one place
type SkillName = 'Tackle' | 'Quick Attack' | 'Iron Tail' | 'Thunderbolt' | 'Thunder';

const SKILL_ATTACK_POINTS: Record<SkillName, number> = {
'Tackle': 10,
'Quick Attack': 20,
'Iron Tail': 30,
'Thunderbolt': 100,
'Thunder': 1000,
} as const;

function getAttackPoint(skill: SkillName): number {
return SKILL_ATTACK_POINTS[skill];
}

function getHp(skill: SkillName, currentHp: number): number {
const attackPoint = getAttackPoint(skill);
return currentHp - attackPoint;
}

Compare with strict equality

In TypeScript, too, use === (the strict equality operator). == (the equality operator) performs implicit type conversion, which can cause unexpected bugs.

Bad

❌ Bad: The equality operator triggers type conversion
// ❌ The equality operator triggers type conversion (an example you can still write in TypeScript)
const input: any = '12';
if (input == 12) { } // true (the string is converted to a number)
if (null == undefined) { } // true (null and undefined are equal)

Note that == comparisons between literals of non-overlapping types, such as '12' == 10 + 2 or 0 == false, are caught by TypeScript itself as a TS2367 compile error. However, comparisons via any like the above, or null == undefined, pass compilation, so standardize on ===.

Good

✅ Good: Use the strict equality operator
// ✅ Use the strict equality operator
if ('12' === String(10 + 2)) { } // Align the types explicitly
if (0 === 0) { } // Compare with the same type
if (value === null || value === undefined) { } // Check explicitly

// ✅ Use ?? or ?. for nullish checks
const result = value ?? 'default';
const name = user?.profile?.name;

Evaluate the most common condition first

Write if conditions starting from the case that occurs most frequently.

Bad

❌ Bad: Evaluating the rare case first
// ❌ Evaluating the rare case first
type UserType = 'free' | 'premium' | 'admin';

function processUser(user: User): void {
if (user.type === 'admin') {
// Process for admin accounts (the fewest)
} else if (user.type === 'premium') {
// Process for premium accounts
} else {
// Process for regular accounts (the most)
}
}

Good

✅ Good: Evaluating the frequent case first
// ✅ Evaluating the frequent case first
function processUser(user: User): void {
if (user.type === 'free') {
// Process for regular accounts (the most)
} else if (user.type === 'premium') {
// Process for premium accounts
} else {
// Process for admin accounts (the fewest)
}
}

Encapsulate boundary conditions

Replacing the values used in boundary conditions with well-named variables improves readability.

Bad

❌ Bad: Magic numbers are scattered
// ❌ Magic numbers are scattered
if (age >= 18 && age < 65) {
console.log('Working-age population');
}

if (items.length > 100) {
console.log('Pagination is needed');
}

Good

✅ Good: Turn boundary conditions into variables
// ✅ Turn boundary conditions into variables
const WORKING_AGE_MIN = 18;
const WORKING_AGE_MAX = 65;
const MAX_ITEMS_PER_PAGE = 100;

const isWorkingAge = age >= WORKING_AGE_MIN && age < WORKING_AGE_MAX;
if (isWorkingAge) {
console.log('Working-age population');
}

const needsPagination = items.length > MAX_ITEMS_PER_PAGE;
if (needsPagination) {
console.log('Pagination is needed');
}

Extract complex conditions into functions

Grouping complex conditions into a function improves readability and enables reuse.

Bad

❌ Bad: The condition is complex and hard to read
// ❌ The condition is complex and hard to read
if (user.age >= 18 && user.isEmailVerified && !user.isBanned && user.subscription.status === 'active') {
// Process
}

// ❌ A regular expression is written directly in the condition
const regex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
if (regex.test(text)) {
console.log('Contains emoji');
}

Good

✅ Good: Extract the condition as a function
// ✅ Extract the condition as a function
function canAccessPremiumContent(user: User): boolean {
return (
user.age >= 18 &&
user.isEmailVerified &&
!user.isBanned &&
user.subscription.status === 'active'
);
}

if (canAccessPremiumContent(user)) {
// Process
}

// ✅ Turn the regular expression into a function too
function containsEmoji(text: string): boolean {
const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
return emojiRegex.test(text);
}

if (containsEmoji(text)) {
console.log('Contains emoji');
}

Use an associative array (object) instead of a switch statement

Switch statements tend to become verbose. Consider replacing them with an associative array using TypeScript's Record type.

Bad

❌ Bad: The switch statement gets long
// ❌ The switch statement gets long
function getAreaName(areaCode: number): string {
switch (areaCode) {
case 1:
return 'Chiyoda';
case 2:
return 'Chuo';
case 3:
return 'Minato';
case 4:
return 'Shinjuku';
default:
return 'Other ward';
}
}

Good

✅ Good: Manage it with an object
// ✅ Manage it with an object
const AREA_NAMES: Record<number, string> = {
1: 'Chiyoda',
2: 'Chuo',
3: 'Minato',
4: 'Shinjuku',
};

function getAreaName(areaCode: number): string {
return AREA_NAMES[areaCode] ?? 'Other ward';
}

// ✅ When making it type-safe
type AreaCode = 1 | 2 | 3 | 4;

const AREA_NAMES_SAFE: Record<AreaCode, string> = {
1: 'Chiyoda',
2: 'Chuo',
3: 'Minato',
4: 'Shinjuku',
};

function getAreaNameSafe(areaCode: AreaCode): string {
return AREA_NAMES_SAFE[areaCode];
}

Use early returns

Using early returns (guard clauses) reduces nesting and improves readability.

Bad

❌ Bad: Deep nesting
// ❌ Deep nesting
async function getUserById(userId: number): Promise<User | null> {
if (typeof userId === 'number' && userId > 0) {
const response = await fetch('/users', {
method: 'POST',
body: JSON.stringify({ userId }),
});
if (response.ok) {
const result = await response.json();
return result;
}
return null;
}
return null;
}

Good

✅ Good: Flat with early returns
// ✅ Flat with early returns
async function getUserById(userId: number): Promise<User | null> {
if (typeof userId !== 'number' || userId <= 0) {
return null;
}

const response = await fetch('/users', {
method: 'POST',
body: JSON.stringify({ userId }),
});

if (!response.ok) {
return null;
}

return response.json();
}

Prevent nesting

Deeply nested code is hard to read and a source of bugs.

Bad

❌ Bad: Deep nesting
// ❌ Deep nesting
function isActiveUser(user: User | null): boolean {
if (user != null) {
if (user.startDate <= today && (user.endDate == null || today <= user.endDate)) {
if (user.stopped) {
return false;
} else {
return true;
}
} else {
return false;
}
} else {
return false;
}
}

Good

✅ Good: Flat with early returns
// ✅ Flat with early returns
function isActiveUser(user: User | null): boolean {
if (user === null) return false;
if (user.startDate > today) return false;
if (user.endDate !== null && today > user.endDate) return false;
if (user.stopped) return false;

return true;
}

// ✅ Turn the condition into a function as well
function isWithinActivePeriod(user: User, today: Date): boolean {
return user.startDate <= today && (user.endDate === null || today <= user.endDate);
}

function isActiveUser(user: User | null): boolean {
if (user === null) return false;
if (!isWithinActivePeriod(user, today)) return false;
if (user.stopped) return false;

return true;
}

Branching with TypeScript types

In TypeScript, you can branch safely using types.

Discriminated Union

✅ Good: Discriminate with a type property
// ✅ Discriminate with a type property
type Shape =
| { type: 'circle'; radius: number }
| { type: 'rectangle'; width: number; height: number }
| { type: 'triangle'; base: number; height: number };

function calculateArea(shape: Shape): number {
switch (shape.type) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}

Exhaustiveness check

✅ Good: The compiler confirms that every case is handled
// ✅ The compiler confirms that every case is handled
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}

type Status = 'pending' | 'approved' | 'rejected';

function getStatusMessage(status: Status): string {
switch (status) {
case 'pending':
return 'Awaiting approval';
case 'approved':
return 'Approved';
case 'rejected':
return 'Rejected';
default:
return assertNever(status); // A compile error occurs if a new Status is added
}
}

Reduce if statements with callbacks or classes

Reducing if statements lowers code complexity.

Bad

❌ Bad: Branching the process with an if statement
// ❌ Branching the process with an if statement
function executeQuery(type: 'username' | 'password', data: UserData): void {
const db = getDBConnection();
db.startTransaction();

if (type === 'username') {
updateUsername(data, db);
} else if (type === 'password') {
updatePassword(data, db);
}

db.endTransaction();
}

Good

✅ Good: Inject the process with a callback function
// ✅ Inject the process with a callback function
type UpdateFunction = (data: UserData, db: Database) => void;

function executeQuery(updateFn: UpdateFunction, data: UserData): void {
const db = getDBConnection();
db.startTransaction();

updateFn(data, db);

db.endTransaction();
}

// Usage
executeQuery(updateUsername, { userId: 1, userName: 'Tom' });
executeQuery(updatePassword, { userId: 1, password: 'secret' });

Remove if statements with polymorphism

✅ Good: Eliminate branches with interfaces and classes
// ✅ Eliminate branches with interfaces and classes
interface Animal {
eat(): void;
sleep(): void;
}

class Dog implements Animal {
eat(): void {
console.log('Eat dog food');
}
sleep(): void {
console.log('Sleep in the kennel');
}
}

class Cat implements Animal {
eat(): void {
console.log('Eat cat food');
}
sleep(): void {
console.log('Sleep on the sofa');
}
}

function actAnimal(animal: Animal): void {
animal.eat();
animal.sleep();
}

// Usage (no if statements)
const dog = new Dog();
actAnimal(dog);

const cat = new Cat();
actAnimal(cat);

Exercises

Refactor the following code with early returns
// Problem
function isCorrectInput(member: Member): boolean {
if (member.id) {
if (member.name) {
if (member.age) {
if (member.email) {
return true;
} else {
console.error('Email address is not entered');
return false;
}
} else {
console.error('Age is not entered');
return false;
}
} else {
console.error('Name is not entered');
return false;
}
} else {
console.error('id is missing');
return false;
}
}

Answer:

interface ValidationResult {
isValid: boolean;
error?: string;
}

function validateMember(member: Member): ValidationResult {
if (!member.id) {
return { isValid: false, error: 'id is missing' };
}
if (!member.name) {
return { isValid: false, error: 'Name is not entered' };
}
if (!member.age) {
return { isValid: false, error: 'Age is not entered' };
}
if (!member.email) {
return { isValid: false, error: 'Email address is not entered' };
}
return { isValid: true };
}

function isCorrectInput(member: Member): boolean {
const result = validateMember(member);
if (!result.isValid && result.error) {
console.error(result.error);
}
return result.isValid;
}

Improvements:

  1. Eliminate nesting with early returns
  2. Separate the validation logic
  3. Structure the error messages

Summary of conditionals

Best practices for conditionals
  • Keep branches in one place
  • Use the strict equality operator (===)
  • Evaluate the frequent case first
  • Turn boundary conditions into variables
  • Extract complex conditions into functions
  • Consider an object instead of a switch statement
  • Reduce nesting with early returns
  • Stay type-safe with discriminated unions
  • Reduce if statements with polymorphism