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 5: Using Partial and Omit

Scenario

You need to filter users by various criteria, but requiring all User properties for the filter would be impractical. You should be able to filter by just age, or just name, or any combination.

The Problem

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

export function filterUsers(persons: Person[], criteria: User): User[] {
    return persons.filter(isUser).filter((user) => {
        const criteriaKeys = Object.keys(criteria) as (keyof User)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

// ❌ Error: Must provide all User properties
filterUsers(persons, {
    age: 23  // Missing name, type, occupation
});
The criteria parameter requires a complete User object, but you only want to filter by specific fields.

Key Concepts

Partial<T>

Makes all properties of type T optional

Omit<T, K>

Creates a type with all properties of T except those in K

Understanding Utility Types

TypeScript provides built-in utility types for common type transformations:
interface User {
    name: string;
    age: number;
    occupation: string;
}

type PartialUser = Partial<User>;
// Equivalent to:
// {
//   name?: string;
//   age?: number;
//   occupation?: string;
// }

Solution

export function filterUsers(persons: Person[], criteria: User): User[] {
    return persons.filter(isUser).filter((user) => {
        const criteriaKeys = Object.keys(criteria) as (keyof User)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

// Must provide all properties
filterUsers(persons, {
    type: 'user',  // Required
    name: '',      // Required
    age: 23,
    occupation: '' // Required
});

Breaking Down the Solution

Let’s understand Partial<Omit<User, 'type'>> step by step:
1

Start with User

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

Apply Omit

Omit<User, 'type'>
// Result:
// {
//   name: string;
//   age: number;
//   occupation: string;
// }
Removes the ‘type’ field since you don’t want to filter by it
3

Apply Partial

Partial<Omit<User, 'type'>>
// Result:
// {
//   name?: string;
//   age?: number;
//   occupation?: string;
// }
Makes all remaining fields optional
The type field is a discriminant used to identify the object type. When filtering users, you already know they’re users (after persons.filter(isUser)), so filtering by type: 'user' is redundant and could lead to mistakes.

Practical Usage

const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
    { type: 'user', name: 'Wilson', age: 23, occupation: 'Ball' }
];

console.log('Users of age 23:');
filterUsers(persons, { age: 23 }).forEach(logPerson);
// Output:
//  - Kate Müller, 23, Astronaut
//  - Wilson, 23, Ball

console.log('Astronauts:');
filterUsers(persons, { occupation: 'Astronaut' }).forEach(logPerson);
// Output:
//  - Kate Müller, 23, Astronaut

Other Useful Utility Types

Pick<T, K>

Select specific properties from a type
type UserName = Pick<User, 'name' | 'age'>;

Required<T>

Makes all properties required
type CompleteUser = Required<PartialUser>;

Readonly<T>

Makes all properties read-only
type ImmutableUser = Readonly<User>;

Record<K, T>

Creates an object type with keys K and values T
type UserMap = Record<string, User>;

Combining Utility Types

You can chain utility types for complex transformations:
// Optional fields except 'name'
type PartialExceptName = Partial<User> & Pick<User, 'name'>;

// Required 'name' and 'age', optional others
type RequiredNameAge = Partial<User> & Required<Pick<User, 'name' | 'age'>>;

// Read-only partial user
type ReadonlyPartialUser = Readonly<Partial<User>>;
Utility types compose well together. Think of them as building blocks for creating exactly the type you need.

Type-Safe Object Keys

Notice the type assertion for object keys:
const criteriaKeys = Object.keys(criteria) as (keyof Omit<User, 'type'>)[];
Object.keys() returns string[] because JavaScript objects can have string keys added at runtime. TypeScript plays it safe by returning the wider type.When you know the object’s structure (like here with criteria), you can safely assert to the specific keys:
// Object.keys returns: string[]
// We assert to: ('name' | 'age' | 'occupation')[]
This allows type-safe property access in the loop.

Common Patterns

function filterUsers(
    persons: Person[],
    criteria: Partial<Omit<User, 'type'>>
): User[] {
    // Implementation
}

What You Learned

Makes all properties optional, useful for filters, updates, and partial data
Removes specific properties from a type, useful for excluding discriminants or auto-generated fields
Utility types can be combined to create precisely the types you need
No need to duplicate type definitions - derive new types from existing ones

Next Steps

Move on to Generics to learn how to create reusable, type-safe functions that work with any type.

Additional Resources