Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/typescript-exercises/typescript-exercises/llms.txt

Use this file to discover all available pages before exploring further.

Exercise 4: Type Predicates

Scenario

You’ve added a type field to distinguish Users from Admins and extracted type checking logic into separate functions. However, TypeScript doesn’t understand that these functions narrow the type.

The Problem

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

export type Person = User | Admin;

export function isAdmin(person: Person) {
    return person.type === 'admin';
}

export function isUser(person: Person) {
    return person.type === 'user';
}

export function logPerson(person: Person) {
    let additionalInformation: string = '';
    if (isAdmin(person)) {
        additionalInformation = person.role;  // ❌ Error: Property 'role' doesn't exist on Person
    }
    if (isUser(person)) {
        additionalInformation = person.occupation;  // ❌ Error
    }
    console.log(`${person.name}, ${person.age}, ${additionalInformation}`);
}
Even though isAdmin and isUser correctly identify the type, TypeScript doesn’t know this without explicit type predicates.

Key Concepts

Discriminated Unions

Union types with a common literal property (discriminant) for type narrowing

Type Predicates

Functions that return value is Type to tell TypeScript about type narrowing

Understanding Type Predicates

A type predicate is a special return type that tells TypeScript the function narrows the type:
function isAdmin(person: Person): person is Admin {
    //                            ^^^^^^^^^^^^^^^^ Type predicate
    return person.type === 'admin';
}
The syntax person is Admin means:
  • If the function returns true, TypeScript narrows person to Admin
  • If it returns false, TypeScript knows person is not Admin

Solution

export function isAdmin(person: Person) {
    return person.type === 'admin';
}

export function isUser(person: Person) {
    return person.type === 'user';
}

export function logPerson(person: Person) {
    let additionalInformation: string = '';
    if (isAdmin(person)) {
        additionalInformation = person.role;  // ❌ Error
    }
    if (isUser(person)) {
        additionalInformation = person.occupation;  // ❌ Error
    }
    console.log(`${person.name}, ${person.age}, ${additionalInformation}`);
}

Discriminated Unions Explained

This pattern uses a discriminant field (the type property) to distinguish union members:
interface User {
    type: 'user';  // Literal type - discriminant
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';  // Different literal - discriminant
    name: string;
    age: number;
    role: string;
}
Literal types like 'user' and 'admin' (instead of string) provide:
  1. Exhaustiveness checking - TypeScript ensures you handle all cases
  2. Autocomplete - IDEs suggest the exact values
  3. Type narrowing - Direct checks like person.type === 'admin' narrow the type

Using Type Predicates with Array Methods

Type predicates shine when filtering arrays:
const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    { type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
    { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
    { type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' }
];

console.log('Admins:');
persons.filter(isAdmin).forEach(logPerson);
//              ^^^^^^^ Returns Admin[], not Person[]

console.log('Users:');
persons.filter(isUser).forEach(logPerson);
//             ^^^^^^ Returns User[], not Person[]
Without type predicates, persons.filter(isAdmin) would still return Person[], defeating the purpose of filtering.

How It Works

1

Define Type Predicate

function isAdmin(person: Person): person is Admin
2

Call in Condition

if (isAdmin(person)) { ... }
3

Type Narrowed

Inside the block, TypeScript knows person is Admin
4

Safe Property Access

Now you can access person.role without errors

Type Predicates vs in Operator

function isAdmin(person: Person): person is Admin {
    return person.type === 'admin';
}

// Can be used anywhere
if (isAdmin(person)) { ... }
persons.filter(isAdmin)
Use type predicates when you need reusable type guards, especially with array methods like filter(), find(), or every().

Common Patterns

TypeScript can verify you’ve handled all union cases:
function handlePerson(person: Person) {
    if (isAdmin(person)) {
        return `Admin: ${person.role}`;
    } else if (isUser(person)) {
        return `User: ${person.occupation}`;
    }
    // If you add a new Person type, TypeScript will error here
    const _exhaustive: never = person;
    return _exhaustive;
}

Real-World Benefits

// Before: Unsafe, requires runtime checks
function getRole(person: any) {
    return person.role || person.occupation || 'Unknown';
}

// After: Type-safe, compile-time guarantees
function getRole(person: Person): string {
    if (isAdmin(person)) {
        return person.role;
    }
    return person.occupation;
}

What You Learned

Functions with parameter is Type return type enable type narrowing
Common literal properties make unions easy to narrow and type-safe
Type predicates work with array methods and can be called anywhere

Next Steps

Continue to Merged Types to learn about utility types like Partial and Omit for flexible type composition.

Additional Resources