TypeScript

Mastering TypeScript Generics

Dinesh SutiharJanuary 30, 202611 min read
TypeScriptJavaScriptProgrammingWeb Development
Mastering TypeScript Generics

What Are Generics?

Generics allow you to write reusable code that works with multiple types while maintaining type safety. Instead of using 'any' or duplicating code for each type, generics let you create flexible, type-safe abstractions.

Basic Generic Syntax

typescript
// Generic function
function identity<T>(value:T):T {
return value;
}
 
const num = identity<number>(42); // T is number
const str = identity("hello"); // T inferred as string
 
// Generic interface
interface Box<T> {
value:T;
getValue():T;
}
 
const numBox:Box<number> = {
value: 42,
getValue() { return this.value; }
};

Generic Constraints

Constraints limit the types that can be used with a generic. Use 'extends' to specify requirements.

typescript
// Constraint: T must have a length property
interface HasLength {
length: number;
}
 
function logLength<T extends HasLength>(item:T): number {
console.log(item.length);
return item.length;
}
 
logLength("hello"); // ✓ strings have length
logLength([1, 2, 3]); // ✓ arrays have length
logLength({ length: 10 }); // ✓ object with length
// logLength(123); // ✗ numbers don't have length
 
// Constraint with keyof
function getProperty<T, K extends keyof T>(obj:T, key:K):T[K] {
return obj[key];
}
 
const person = { name: "Alice", age: 30 };
getProperty(person, "name"); // ✓ returns string
getProperty(person, "age"); // ✓ returns number
// getProperty(person, "email"); // ✗ "email" not in person

Multiple Type Parameters

typescript
// Tuple creation with two types
function pair<T, U>(first:T, second:U): [T, U] {
return [first, second];
}
 
const p = pair("hello", 42); // [string, number]
 
// Practical example: Map function
function mapArray<T, U>(arr:T[], fn: (item:T) => U):U[] {
return arr.map(fn);
}
 
const nums = [1, 2, 3];
const strings = mapArray(nums, n => n.toString()); // string[]

Generic Classes

typescript
class Stack<T> {
private items:T[] = [];
push(item:T): void {
this.items.push(item);
}
pop():T | undefined {
return this.items.pop();
}
peek():T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
 
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined

Built-in Utility Types

TypeScript provides powerful utility types that leverage generics:

typescript
interface User {
id: number;
name: string;
email: string;
password: string;
}
 
// Partial - all properties optional
type PartialUser = Partial<User>;
 
// Required - all properties required
type RequiredUser = Required<PartialUser>;
 
// Pick - select specific properties
type UserCredentials = Pick<User, 'email' | 'password'>;
 
// Omit - exclude specific properties
type PublicUser = Omit<User, 'password'>;
 
// Record - create object type with specific keys
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
 
// ReturnType - extract return type of function
function getUser() { return { id: 1, name: 'Alice' }; }
type UserReturn = ReturnType<typeof getUser>;

Conditional Types

typescript
// Basic conditional type
type IsString<T> = T extends string ? true : false;
 
type A = IsString<string>; // true
type B = IsString<number>; // false
 
// Practical: Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;
 
type NumElement = ElementType<number[]>; // number
type StrElement = ElementType<string[]>; // string
 
// Exclude and Extract
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'

Real-World Pattern: API Response

typescript
// Generic API response wrapper
interface ApiResponse<T> {
data:T;
success: boolean;
message?: string;
timestamp:Date;
}
 
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
};
}
 
// Usage with async functions
async function fetchUser(id: string):Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
 
async function fetchUsers():Promise<PaginatedResponse<User>> {
const res = await fetch('/api/users');
return res.json();
}

Conclusion

Generics are essential for writing scalable TypeScript code. Start with simple generic functions, then gradually incorporate constraints and utility types. The key is finding the right balance between flexibility and type safety.

Share this article