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.

Generics enable you to write flexible, reusable code while maintaining type safety. These exercises progress from simple generic functions to advanced patterns like promisification.

Exercise 6: Function Overloads with Generics

Scenario

You need filterPersons to return User[] when filtering users and Admin[] when filtering admins. The return type should match the personType parameter.

The Problem

export function filterPersons(
    persons: Person[], 
    personType: string, 
    criteria: unknown
): unknown[] {
    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];
            });
        });
}

const users = filterPersons(persons, 'user', { age: 23 });  // unknown[]
const admins = filterPersons(persons, 'admin', { age: 23 }); // unknown[]
The function returns unknown[] regardless of the personType, losing all type information.

Key Concepts

Function Overloads

Multiple function signatures for different parameter combinations

Type Correlation

Making return types depend on parameter values

Solution

export function filterPersons(
    persons: Person[], 
    personType: string, 
    criteria: unknown
): unknown[] {
    // Implementation
}

const users = filterPersons(persons, 'user', { age: 23 });
// Type: unknown[]
Create a generic helper for type-safe object keys:
const getObjectKeys = <T>(obj: T) => Object.keys(obj) as (keyof T)[];

// Usage
const keys = getObjectKeys(criteria);  // ('name' | 'age' | 'occupation')[]

Exercise 7: Basic Generics with Tuples

Scenario

Implement a swap function that takes two values and returns them in reverse order, preserving their types.

The Problem

export function swap(v1, v2) {  // No types!
    return [v2, v1];
}

const [admin, user] = swap(users[0], admins[1]);
// Both have type 'any'

Solution

Use generics to capture and preserve input types:
export function swap(v1, v2) {
    return [v2, v1];
}
Tuple types like [T2, T1] preserve the exact order and type of each element, unlike arrays which have a single element type.

Exercise 8: Intersection Types with Generics

Scenario

Create a PowerUser type that combines all fields from both User and Admin (except type).

The Problem

type PowerUser = unknown;

const powerUser: PowerUser = {
    type: 'powerUser',
    name: 'Nikki Stone',
    age: 45,
    role: 'Moderator',
    occupation: 'Cat groomer'
};

Solution

Use intersection types and Omit to merge interfaces:
type PowerUser = unknown;

Exercise 9: Generic API Response Type

Scenario

Create a generic ApiResponse<T> type to handle success and error responses uniformly.

The Problem

type AdminsApiResponse = (
    { status: 'success'; data: Admin[]; } |
    { status: 'error'; error: string; }
);

type UsersApiResponse = (
    { status: 'success'; data: User[]; } |
    { status: 'error'; error: string; }
);

// Duplication for every data type!

Solution

Create a generic type that works for any data:
// Duplicated for each type
type AdminsApiResponse = { status: 'success'; data: Admin[]; } | { status: 'error'; error: string; };
type UsersApiResponse = { status: 'success'; data: User[]; } | { status: 'error'; error: string; };

export function requestAdmins(callback: (response: AdminsApiResponse) => void) {
    // ...
}

export function requestUsers(callback: (response: UsersApiResponse) => void) {
    // ...
}

Using Generic API Responses

function startTheApp(callback: (error: Error | null) => void) {
    requestAdmins((adminsResponse) => {
        if (adminsResponse.status === 'success') {
            adminsResponse.data.forEach(logPerson);  // data is Admin[]
        } else {
            return callback(new Error(adminsResponse.error));  // error is string
        }
        
        requestUsers((usersResponse) => {
            if (usersResponse.status === 'success') {
                usersResponse.data.forEach(logPerson);  // data is User[]
            } else {
                return callback(new Error(usersResponse.error));
            }
        });
    });
}

Exercise 10: Advanced Generics - Promisify

Scenario

Convert callback-based async functions to Promise-based ones, preserving type safety.

The Problem

export function promisify(arg: unknown): unknown {
    return null;
}

const api = {
    requestAdmins: promisify(oldApi.requestAdmins),  // unknown
    requestUsers: promisify(oldApi.requestUsers)     // unknown
};

Solution

export function promisify(arg: unknown): unknown {
    return null;
}

Using the Promisified API

async function startTheApp() {
    console.log('Admins:');
    (await api.requestAdmins()).forEach(logPerson);
    
    console.log('Users:');
    (await api.requestUsers()).forEach(logPerson);
    
    console.log('Server time:');
    console.log(`   ${new Date(await api.requestCurrentServerTime()).toLocaleString()}`);
}

startTheApp().then(
    () => console.log('Success!'),
    (e: Error) => console.log(`Error: "${e.message}"`)
);
Create a function that promisifies all methods in an object:
type SourceObject<T> = {[K in keyof T]: CallbackBasedAsyncFunction<T[K]>};
type PromisifiedObject<T> = {[K in keyof T]: PromiseBasedAsyncFunction<T[K]>};

export function promisifyAll<T extends {[key: string]: any}>(
    obj: SourceObject<T>
): PromisifiedObject<T> {
    const result: Partial<PromisifiedObject<T>> = {};
    for (const key of Object.keys(obj) as (keyof T)[]) {
        result[key] = promisify(obj[key]);
    }
    return result as PromisifiedObject<T>;
}

// Usage
const api = promisifyAll(oldApi);

Key Generic Patterns

Single Generic

function identity<T>(value: T): T {
    return value;
}

Multiple Generics

function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}

Generic Constraints

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

Generic Types

type ApiResponse<T> = 
    | { success: true; data: T }
    | { success: false; error: string };

What You Learned

Define multiple type signatures for functions that behave differently based on parameters
Create reusable functions that work with any type while preserving type safety
Define flexible type structures that can wrap any data type
Convert between different function signatures while maintaining types
Transform all properties in an object type systematically

Next Steps

Continue to Type Declarations to learn how to write declaration files for JavaScript libraries.

Additional Resources