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.

What are Utility Types?

Utility types are built-in generic types that help transform and manipulate existing types. They enable powerful type transformations without code duplication.

Essential Utility Types

Partial<Type>

Makes all properties of a type optional. From Exercise 5, here’s a practical example:
interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

// Partial makes all properties optional
type PartialUser = Partial<User>;
// Equivalent to:
// {
//     type?: 'user';
//     name?: string;
//     age?: number;
//     occupation?: string;
// }

function filterUsers(
    persons: Person[],
    criteria: Partial<User>  // Can pass any subset of User properties
): User[] {
    return persons.filter(isUser).filter((user) => {
        const criteriaKeys = Object.keys(criteria) as (keyof User)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

// Usage - can filter by any combination of properties
filterUsers(persons, { age: 23 });
filterUsers(persons, { age: 23, occupation: 'Astronaut' });
Partial is perfect for:
  • Update functions that accept partial data
  • Filter/search criteria objects
  • Configuration objects with defaults
  • Optional patch operations

Required<Type>

Makes all properties of a type required (opposite of Partial):
interface User {
    name: string;
    age?: number;
    email?: string;
}

type RequiredUser = Required<User>;
// {
//     name: string;
//     age: number;      // No longer optional
//     email: string;    // No longer optional
// }

Readonly<Type>

Makes all properties readonly:
interface User {
    name: string;
    age: number;
}

type ReadonlyUser = Readonly<User>;
// {
//     readonly name: string;
//     readonly age: number;
// }

const user: ReadonlyUser = { name: 'John', age: 30 };
// user.name = 'Jane'; // Error: Cannot assign to 'name' because it is a read-only property

Pick<Type, Keys>

Creates a type by picking specific properties from another type:
interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
    email: string;
    password: string;
}

// Pick only the properties we want to display
type UserProfile = Pick<User, 'name' | 'age' | 'occupation'>;
// {
//     name: string;
//     age: number;
//     occupation: string;
// }

function displayProfile(user: UserProfile) {
    console.log(`${user.name}, ${user.age}, ${user.occupation}`);
}
Pick is useful for:
  • Creating DTOs (Data Transfer Objects)
  • Selecting fields for API responses
  • Creating view models from domain models
  • Type-safe property selection

Omit<Type, Keys>

From Exercise 5, creates a type by omitting specific properties:
interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

// Omit the 'type' field for filter criteria
type UserCriteria = Omit<User, 'type'>;
// {
//     name: string;
//     age: number;
//     occupation: string;
// }

function filterUsers(
    persons: Person[],
    criteria: Partial<Omit<User, 'type'>>  // Can't filter by type
): User[] {
    return persons.filter(isUser).filter((user) => {
        const criteriaKeys = Object.keys(criteria) as (keyof Omit<User, 'type'>)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

// Usage
filterUsers(persons, { age: 23 });  // ✓ Works
// filterUsers(persons, { type: 'user' });  // ✗ Error: 'type' is not in UserCriteria
// Pick: Select specific properties
type UserBasic = Pick<User, 'name' | 'age'>;

// Use when you know what to include

Record<Keys, Type>

Creates an object type with specified keys and value type:
type Role = 'admin' | 'user' | 'guest';

type Permissions = Record<Role, string[]>;
// {
//     admin: string[];
//     user: string[];
//     guest: string[];
// }

const permissions: Permissions = {
    admin: ['read', 'write', 'delete'],
    user: ['read', 'write'],
    guest: ['read']
};

// Another example: mapping user IDs to users
type UserMap = Record<string, User>;

const users: UserMap = {
    'user-1': { type: 'user', name: 'John', age: 30, occupation: 'Developer' },
    'user-2': { type: 'user', name: 'Jane', age: 25, occupation: 'Designer' }
};
Record is perfect for:
  • Creating lookup tables/maps
  • Ensuring all enum values are handled
  • Type-safe dictionaries
  • Configuration objects

ReturnType<Type>

Extracts the return type of a function:
function getUser() {
    return {
        name: 'John',
        age: 30,
        occupation: 'Developer'
    };
}

type User = ReturnType<typeof getUser>;
// {
//     name: string;
//     age: number;
//     occupation: string;
// }

// Another example from Exercise 4
function isAdmin(person: Person): person is Admin {
    return person.type === 'admin';
}

type IsAdminReturn = ReturnType<typeof isAdmin>;  // boolean

Parameters<Type>

Extracts function parameter types as a tuple:
function filterUsers(
    persons: Person[],
    criteria: Partial<User>
): User[] {
    // ...
}

type FilterUsersParams = Parameters<typeof filterUsers>;
// [persons: Person[], criteria: Partial<User>]

// Access individual parameters
type FirstParam = FilterUsersParams[0];   // Person[]
type SecondParam = FilterUsersParams[1];  // Partial<User>

Awaited<Type>

Extracts the type that a Promise resolves to:
type AsyncUser = Promise<User>;
type ResolvedUser = Awaited<AsyncUser>;  // User

// Works with nested Promises
type NestedPromise = Promise<Promise<User>>;
type Resolved = Awaited<NestedPromise>;  // User

async function fetchUser(): Promise<User> {
    // ...
}

type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;  // User

Combining Utility Types

From Exercise 5, utility types can be combined for complex transformations:
interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

// Combine Partial and Omit
type UserFilterCriteria = Partial<Omit<User, 'type'>>;
// {
//     name?: string;
//     age?: number;
//     occupation?: string;
// }

// Combine Pick and Readonly
type UserDisplayInfo = Readonly<Pick<User, 'name' | 'age'>>;
// {
//     readonly name: string;
//     readonly age: number;
// }
When combining utility types, the order matters:
// Different results:
type A = Partial<Omit<User, 'type'>>;  // Omit first, then make optional
type B = Omit<Partial<User>, 'type'>;  // Make optional first, then omit

// A has optional properties (better for our use case)
// B omits an already-optional property

Advanced Utility Types

Exclude<Type, ExcludedUnion>

Excludes types from a union:
type Person Type = 'user' | 'admin' | 'guest';

type ActivePersonType = Exclude<PersonType, 'guest'>;
// 'user' | 'admin'

// Practical example
type Person = User | Admin | Guest;
type ActivePerson = Exclude<Person, Guest>;

Extract<Type, Union>

Extracts types from a union:
type PersonType = 'user' | 'admin' | 'guest';

type PrivilegedType = Extract<PersonType, 'user' | 'admin'>;
// 'user' | 'admin'

NonNullable<Type>

Removes null and undefined from a type:
type MaybeUser = User | null | undefined;

type DefiniteUser = NonNullable<MaybeUser>;
// User

function processUser(user: MaybeUser) {
    if (user) {
        const definiteUser: NonNullable<MaybeUser> = user;
        // definiteUser is guaranteed to be User
    }
}

Creating Custom Utility Types

From Exercise 15, you can create your own utility types:
// Add a new property to a type
type ObjectWithNewProp<T, K extends string, V> = T & { [NK in K]: V };

// Usage
type UserWithEmail = ObjectWithNewProp<User, 'email', string>;
// {
//     type: 'user';
//     name: string;
//     age: number;
//     occupation: string;
//     email: string;
// }

Practical Patterns

API Response Types

From Exercise 10:
type ApiResponse<T> = 
    | { status: 'success'; data: T }
    | { status: 'error'; error: string };

// Extract just the success data type
type SuccessData<T> = Extract<ApiResponse<T>, { status: 'success' }>['data'];

type UserData = SuccessData<User>;  // User

Form Handling

interface User {
    id: string;
    name: string;
    email: string;
    age: number;
}

// For creating new users (omit id, it's generated)
type CreateUserInput = Omit<User, 'id'>;

// For updating users (all fields optional except id)
type UpdateUserInput = Pick<User, 'id'> & Partial<Omit<User, 'id'>>;

function createUser(input: CreateUserInput): User {
    return {
        id: generateId(),
        ...input
    };
}

function updateUser(input: UpdateUserInput): User {
    // Update only provided fields
}

Function Overloads with Utility Types

From Exercise 6:
function filterPersons(
    persons: Person[],
    personType: 'user',
    criteria: Partial<Omit<User, 'type'>>
): User[];

function filterPersons(
    persons: Person[],
    personType: 'admin',
    criteria: Partial<Omit<Admin, 'type'>>
): Admin[];

function filterPersons(
    persons: Person[],
    personType: string,
    criteria: Partial<Person>
): Person[] {
    return persons
        .filter((person) => person.type === personType)
        .filter((person) => {
            let criteriaKeys = Object.keys(criteria) as (keyof Person)[];
            return criteriaKeys.every((fieldName) => {
                return person[fieldName] === criteria[fieldName];
            });
        });
}

// TypeScript knows the return type!
const users = filterPersons(persons, 'user', { age: 23 });  // User[]
const admins = filterPersons(persons, 'admin', { age: 23 });  // Admin[]

Best Practices

Instead of defining similar types manually:
// Bad - Manual duplication
interface User {
    name: string;
    age: number;
    email: string;
}

interface PartialUser {
    name?: string;
    age?: number;
    email?: string;
}

// Good - Use utility types
interface User {
    name: string;
    age: number;
    email: string;
}

type PartialUser = Partial<User>;
Build complex types from simple utility types:
// Create, Update, and Display types from a single source
interface User {
    id: string;
    name: string;
    email: string;
    password: string;
    createdAt: Date;
}

type CreateUser = Omit<User, 'id' | 'createdAt'>;
type UpdateUser = Partial<Omit<User, 'id' | 'createdAt'>> & Pick<User, 'id'>;
type UserDisplay = Omit<User, 'password'>;
For repeated patterns, create reusable utility types:
// Generic "WithoutType" utility
type WithoutType<T> = Omit<T, 'type'>;

// Generic "Criteria" utility for filtering
type Criteria<T> = Partial<WithoutType<T>>;

// Usage
type UserCriteria = Criteria<User>;
type AdminCriteria = Criteria<Admin>;

Generics

Learn how utility types are built with generics

Conditional Types

Create your own utility types with conditional logic

Exercise 5

Practice using Partial and Omit utility types

Exercise 6

Combine utility types with function overloads

Further Reading