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[]
// Overload signatures
export function filterPersons (
persons : Person [],
personType : User [ 'type' ],
criteria : Partial < Omit < User , 'type' >>
) : User [];
export function filterPersons (
persons : Person [],
personType : Admin [ 'type' ],
criteria : Partial < Omit < Admin , 'type' >>
) : Admin [];
// Implementation signature
export function filterPersons (
persons : Person [],
personType : Person [ 'type' ],
criteria : Partial < Person >
) : Person [] {
return persons
. filter (( person ) => person . type === personType )
. filter (( person ) => {
let criteriaKeys = getObjectKeys ( criteria );
return criteriaKeys . every (( fieldName ) => {
return person [ fieldName ] === criteria [ fieldName ];
});
});
}
const users = filterPersons ( persons , 'user' , { age: 23 }); // User[]
const admins = filterPersons ( persons , 'admin' , { age: 23 }); // Admin[]
Bonus: getObjectKeys helper
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 ];
}
export function swap < T1 , T2 >( v1 : T1 , v2 : T2 ) : [ T2 , T1 ] {
return [ v2 , v1 ];
}
// Usage
const [ user , admin ] = swap ( admins [ 0 ], users [ 1 ]);
// user: User, admin: Admin
const [ str , num ] = swap ( 123 , 'Hello' );
// str: string, num: number
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 ;
type PowerUser = Omit < User , 'type' > & Omit < Admin , 'type' > & {
type : 'powerUser'
};
// Equivalent to:
// {
// type: 'powerUser';
// name: string;
// age: number;
// occupation: string; // from User
// role: string; // from Admin
// }
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 ) {
// ...
}
export type ApiResponse < T > =
| { status : 'success' ; data : T ; }
| { status : 'error' ; error : string ; };
export function requestAdmins (
callback : ( response : ApiResponse < Admin []>) => void
) {
callback ({ status: 'success' , data: admins });
}
export function requestUsers (
callback : ( response : ApiResponse < User []>) => void
) {
callback ({ status: 'success' , data: users });
}
export function requestCurrentServerTime (
callback : ( response : ApiResponse < number >) => void
) {
callback ({ status: 'success' , data: Date . now () });
}
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 ;
}
type CallbackBasedAsyncFunction < T > = (
callback : ( response : ApiResponse < T >) => void
) => void ;
type PromiseBasedAsyncFunction < T > = () => Promise < T >;
export function promisify < T >(
fn : CallbackBasedAsyncFunction < T >
) : PromiseBasedAsyncFunction < T > {
return () => new Promise < T >(( resolve , reject ) => {
fn (( response ) => {
if ( response . status === 'success' ) {
resolve ( response . data );
} else {
reject ( new Error ( response . error ));
}
});
});
}
const api = {
requestAdmins: promisify ( oldApi . requestAdmins ), // () => Promise<Admin[]>
requestUsers: promisify ( oldApi . requestUsers ), // () => Promise<User[]>
requestCurrentServerTime: promisify ( oldApi . requestCurrentServerTime ) // () => Promise<number>
};
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