Master TypeScript generics, constraints, and advanced type manipulation. Learn to create flexible, reusable code that maintains type safety across different data types and complex scenarios.
Generics are TypeScript's most powerful feature for creating reusable, type-safe code. They allow you to write functions, classes, and interfaces that work with multiple types while preserving type information and relationships.
Think of generics as "type variables" - placeholders that get filled in with actual types when you use them. This enables you to create utilities that work with any type while maintaining complete type safety, preventing the need for any types or code duplication.
In this lesson, you'll master everything from basic generic functions to advanced patterns like conditional types, mapped types, and utility types that power TypeScript's built-in type system.
Generics enable writing reusable code that works with multiple types while maintaining type safety and relationships between inputs and outputs.
Basic Generic Functions:
1// Generic function with single type parameter
2function identity<T>(arg: T): T {
3 return arg;
4}
5
6// Usage with explicit type
7const stringResult = identity<string>("hello"); // Type: string
8const numberResult = identity<number>(42); // Type: number
9
10// Usage with type inference
11const autoString = identity("world"); // Type inferred as string
12const autoNumber = identity(123); // Type inferred as number
13
14// Generic function with multiple parameters
15function combine<T, U>(first: T, second: U): { first: T; second: U } {
16 return { first, second };
17}
18
19const combined = combine("hello", 42); // Type: { first: string; second: number }Generic Classes:
1class Container<T> {
2 private items: T[] = [];
3
4 add(item: T): void {
5 this.items.push(item);
6 }
7
8 get(index: number): T | undefined {
9 return this.items[index];
10 }
11
12 getAll(): T[] {
13 return [...this.items];
14 }
15
16 length(): number {
17 return this.items.length;
18 }
19
20 filter(predicate: (item: T) => boolean): T[] {
21 return this.items.filter(predicate);
22 }
23
24 map<U>(transform: (item: T) => U): U[] {
25 return this.items.map(transform);
26 }
27}
28
29// Usage with different types
30const stringContainer = new Container<string>();
31stringContainer.add("hello");
32stringContainer.add("world");
33
34const numberContainer = new Container<number>();
35numberContainer.add(1);
36numberContainer.add(2);
37
38// Type-safe operations
39const uppercased = stringContainer.map(s => s.toUpperCase()); // string[]
40const doubled = numberContainer.map(n => n * 2); // number[]Generic Interfaces:
1interface Repository<T> {
2 create(item: Omit<T, 'id'>): Promise<T>;
3 findById(id: string): Promise<T | null>;
4 findAll(): Promise<T[]>;
5 update(id: string, updates: Partial<T>): Promise<T | null>;
6 delete(id: string): Promise<boolean>;
7}
8
9interface ApiResponse<T> {
10 data: T;
11 status: number;
12 message: string;
13 timestamp: Date;
14}
15
16interface PaginatedResult<T> {
17 items: T[];
18 total: number;
19 page: number;
20 pageSize: number;
21 hasNext: boolean;
22 hasPrevious: boolean;
23}
24
25// Usage
26interface User {
27 id: string;
28 name: string;
29 email: string;
30}
31
32const userRepository: Repository<User> = {
33 async create(userData) {
34 // Implementation
35 return { id: 'generated-id', ...userData };
36 },
37 // ... other methods
38};
39
40type UserResponse = ApiResponse<User>;
41type UserList = PaginatedResult<User>;Multiple Generic Parameters:
1// Generic function with multiple type parameters
2function merge<T, U, V>(
3 obj1: T,
4 obj2: U,
5 transform: (merged: T & U) => V
6): V {
7 const merged = { ...obj1, ...obj2 } as T & U;
8 return transform(merged);
9}
10
11// Usage
12const person = { name: "John", age: 30 };
13const location = { city: "New York", country: "USA" };
14
15const result = merge(person, location, (merged) => ({
16 fullInfo: `${merged.name} from ${merged.city}, ${merged.country}`,
17 ageInMonths: merged.age * 12
18}));
19
20// result type: { fullInfo: string; ageInMonths: number }Default Generic Parameters:
1// Generic with default type
2interface EventEmitter<T = any> {
3 on(event: string, handler: (data: T) => void): void;
4 emit(event: string, data: T): void;
5}
6
7// Generic function with default
8function createArray<T = string>(length: number, fillValue?: T): T[] {
9 if (fillValue !== undefined) {
10 return Array(length).fill(fillValue);
11 }
12 return Array(length);
13}
14
15// Usage
16const stringArray = createArray(5, "hello"); // string[]
17const defaultArray = createArray(3); // string[]
18const numberArray = createArray<number>(4, 42); // number[]Generic constraints limit what types can be used with generics, while conditional types enable advanced type logic based on type relationships.
Basic Generic Constraints:
1// Constraint using extends
2interface Lengthwise {
3 length: number;
4}
5
6function logLength<T extends Lengthwise>(arg: T): T {
7 console.log(`Length: ${arg.length}`);
8 return arg;
9}
10
11logLength("hello"); // OK: string has length
12logLength([1, 2, 3]); // OK: array has length
13logLength({ length: 10 }); // OK: object has length property
14// logLength(123); // Error: number doesn't have length
15
16// Constraint using keyof
17function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
18 return obj[key];
19}
20
21const person = { name: "John", age: 30, email: "[email protected]" };
22
23const name = getProperty(person, "name"); // Type: string
24const age = getProperty(person, "age"); // Type: number
25// const invalid = getProperty(person, "invalid"); // Error: invalid keyAdvanced Constraints:
1// Multiple constraints
2interface Identifiable {
3 id: string | number;
4}
5
6interface Timestamped {
7 createdAt: Date;
8 updatedAt: Date;
9}
10
11function processEntity<T extends Identifiable & Timestamped>(
12 entity: T
13): T & { processed: boolean } {
14 return {
15 ...entity,
16 processed: true,
17 updatedAt: new Date()
18 };
19}
20
21// Constructor constraints
22interface Constructable {
23 new (...args: any[]): any;
24}
25
26function createInstance<T extends Constructable>(
27 constructor: T,
28 ...args: any[]
29): InstanceType<T> {
30 return new constructor(...args);
31}
32
33class User {
34 constructor(public name: string) {}
35}
36
37class Product {
38 constructor(public name: string, public price: number) {}
39}
40
41const user = createInstance(User, "John"); // Type: User
42const product = createInstance(Product, "Laptop", 999); // Type: ProductConditional Types:
1// Basic conditional type syntax: T extends U ? X : Y
2type IsArray<T> = T extends any[] ? true : false;
3
4type StringArrayCheck = IsArray<string[]>; // true
5type NumberCheck = IsArray<number>; // false
6
7// More complex conditional types
8type NonNullable<T> = T extends null | undefined ? never : T;
9
10type ApiResult<T> = T extends Promise<infer U> ? U : T;
11
12type PromiseValue = ApiResult<Promise<string>>; // string
13type DirectValue = ApiResult<number>; // number
14
15// Conditional types with inference
16type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
17
18type FunctionReturn = ReturnType<() => string>; // string
19type MethodReturn = ReturnType<(x: number) => boolean>; // boolean
20
21// Distributive conditional types
22type ToArray<T> = T extends any ? T[] : never;
23
24type StringOrNumberArray = ToArray<string | number>; // string[] | number[]Mapped Types:
1// Basic mapped type
2type Readonly<T> = {
3 readonly [P in keyof T]: T[P];
4};
5
6type Partial<T> = {
7 [P in keyof T]?: T[P];
8};
9
10type Required<T> = {
11 [P in keyof T]-?: T[P];
12};
13
14// Custom mapped types
15type Stringify<T> = {
16 [K in keyof T]: string;
17};
18
19type Nullify<T> = {
20 [K in keyof T]: T[K] | null;
21};
22
23// Usage
24interface User {
25 id: number;
26 name: string;
27 email: string;
28 age: number;
29}
30
31type ReadonlyUser = Readonly<User>; // All properties readonly
32type PartialUser = Partial<User>; // All properties optional
33type StringUser = Stringify<User>; // All properties as strings
34type NullableUser = Nullify<User>; // All properties nullableTemplate Literal Types:
1// Template literal types for string manipulation
2type EventName<T extends string> = `on${Capitalize<T>}`;
3
4type ClickEvent = EventName<"click">; // "onClick"
5type HoverEvent = EventName<"hover">; // "onHover"
6
7// Complex template literals
8type ApiEndpoint<Resource extends string, Action extends string> =
9 `/api/${Resource}/${Action}`;
10
11type UserEndpoints =
12 | ApiEndpoint<"users", "create">
13 | ApiEndpoint<"users", "update">
14 | ApiEndpoint<"users", "delete">;
15
16// "api/users/create" | "api/users/update" | "api/users/delete"
17
18// Path extraction
19type ExtractPath<T> = T extends `${string}/${infer Path}` ? Path : never;
20
21type UserPath = ExtractPath<"api/users/profile">; // "users/profile"Recursive Types:
1// Recursive type for nested structures
2type DeepReadonly<T> = {
3 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
4};
5
6type DeepPartial<T> = {
7 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
8};
9
10// JSON type
11type JsonValue =
12 | string
13 | number
14 | boolean
15 | null
16 | JsonArray
17 | JsonObject;
18
19interface JsonObject {
20 [key: string]: JsonValue;
21}
22
23interface JsonArray extends Array<JsonValue> {}
24
25// Usage
26interface NestedConfig {
27 database: {
28 host: string;
29 port: number;
30 credentials: {
31 username: string;
32 password: string;
33 };
34 };
35}
36
37type ReadonlyConfig = DeepReadonly<NestedConfig>; // All nested properties readonlyTypeScript provides powerful built-in utility types and patterns for common type transformations and advanced generic scenarios.
Built-in Utility Types:
1interface User {
2 id: string;
3 name: string;
4 email: string;
5 age: number;
6 role: "admin" | "user" | "moderator";
7 preferences: {
8 theme: "light" | "dark";
9 notifications: boolean;
10 };
11}
12
13// Partial - makes all properties optional
14type PartialUser = Partial<User>;
15const updateUser = (id: string, updates: PartialUser) => {
16 // Can update any subset of user properties
17};
18
19// Required - makes all properties required
20type RequiredUser = Required<PartialUser>; // Back to full User type
21
22// Pick - select specific properties
23type UserSummary = Pick<User, "id" | "name" | "email">;
24const displayUser = (user: UserSummary) => {
25 return `${user.name} (${user.email})`;
26};
27
28// Omit - exclude specific properties
29type CreateUserRequest = Omit<User, "id">; // User without id
30const createUser = (userData: CreateUserRequest) => {
31 return { id: generateId(), ...userData };
32};
33
34// Record - create object type with specific keys and values
35type UserRoles = Record<"admin" | "user" | "moderator", string[]>;
36const permissions: UserRoles = {
37 admin: ["read", "write", "delete", "manage"],
38 user: ["read"],
39 moderator: ["read", "write"]
40};
41
42// Exclude - remove types from union
43type NonAdminRoles = Exclude<User["role"], "admin">; // "user" | "moderator"
44
45// Extract - keep only specific types from union
46type AdminRole = Extract<User["role"], "admin">; // "admin"Advanced Generic Patterns:
1// Generic Builder Pattern
2class QueryBuilder<T> {
3 private filters: Array<(item: T) => boolean> = [];
4 private sortFn?: (a: T, b: T) => number;
5 private limitCount?: number;
6
7 where(predicate: (item: T) => boolean): QueryBuilder<T> {
8 this.filters.push(predicate);
9 return this;
10 }
11
12 orderBy<K extends keyof T>(
13 key: K,
14 direction: "asc" | "desc" = "asc"
15 ): QueryBuilder<T> {
16 this.sortFn = (a, b) => {
17 const aVal = a[key];
18 const bVal = b[key];
19 const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
20 return direction === "desc" ? -result : result;
21 };
22 return this;
23 }
24
25 limit(count: number): QueryBuilder<T> {
26 this.limitCount = count;
27 return this;
28 }
29
30 execute(data: T[]): T[] {
31 let result = data.filter(item =>
32 this.filters.every(filter => filter(item))
33 );
34
35 if (this.sortFn) {
36 result = result.sort(this.sortFn);
37 }
38
39 if (this.limitCount) {
40 result = result.slice(0, this.limitCount);
41 }
42
43 return result;
44 }
45}
46
47// Usage
48const users: User[] = [
49 // ... user data
50];
51
52const query = new QueryBuilder<User>()
53 .where(user => user.age > 18)
54 .where(user => user.role !== "admin")
55 .orderBy("name")
56 .limit(10)
57 .execute(users);Generic Event System:
1// Type-safe event system
2type EventMap = Record<string, any[]>;
3
4class TypedEventEmitter<TEvents extends EventMap> {
5 private listeners: {
6 [K in keyof TEvents]?: Array<(...args: TEvents[K]) => void>
7 } = {};
8
9 on<K extends keyof TEvents>(
10 event: K,
11 listener: (...args: TEvents[K]) => void
12 ): void {
13 if (!this.listeners[event]) {
14 this.listeners[event] = [];
15 }
16 this.listeners[event]!.push(listener);
17 }
18
19 emit<K extends keyof TEvents>(
20 event: K,
21 ...args: TEvents[K]
22 ): void {
23 const eventListeners = this.listeners[event];
24 if (eventListeners) {
25 eventListeners.forEach(listener => listener(...args));
26 }
27 }
28
29 off<K extends keyof TEvents>(
30 event: K,
31 listener: (...args: TEvents[K]) => void
32 ): void {
33 const eventListeners = this.listeners[event];
34 if (eventListeners) {
35 const index = eventListeners.indexOf(listener);
36 if (index > -1) {
37 eventListeners.splice(index, 1);
38 }
39 }
40 }
41}
42
43// Define event types
44interface AppEvents {
45 userLogin: [userId: string, timestamp: Date];
46 userLogout: [userId: string];
47 dataUpdate: [entity: string, id: string, changes: Record<string, any>];
48 error: [message: string, code?: number];
49}
50
51const eventEmitter = new TypedEventEmitter<AppEvents>();
52
53// Type-safe event handling
54eventEmitter.on('userLogin', (userId, timestamp) => {
55 console.log(`User ${userId} logged in at ${timestamp}`);
56});
57
58eventEmitter.emit('userLogin', 'user123', new Date());Generic Data Validation:
1// Generic validation system
2type ValidationRule<T, K extends keyof T> = {
3 field: K;
4 validator: (value: T[K]) => boolean;
5 message: string;
6};
7
8type ValidationResult<T> = {
9 valid: boolean;
10 errors: Array<{
11 field: keyof T;
12 message: string;
13 }>;
14};
15
16class Validator<T> {
17 private rules: Array<ValidationRule<T, keyof T>> = [];
18
19 addRule<K extends keyof T>(rule: ValidationRule<T, K>): this {
20 this.rules.push(rule as ValidationRule<T, keyof T>);
21 return this;
22 }
23
24 validate(obj: T): ValidationResult<T> {
25 const errors: ValidationResult<T>["errors"] = [];
26
27 for (const rule of this.rules) {
28 const value = obj[rule.field];
29 if (!rule.validator(value)) {
30 errors.push({
31 field: rule.field,
32 message: rule.message
33 });
34 }
35 }
36
37 return {
38 valid: errors.length === 0,
39 errors
40 };
41 }
42}
43
44// Usage
45interface RegisterForm {
46 username: string;
47 email: string;
48 password: string;
49 age: number;
50}
51
52const userValidator = new Validator<RegisterForm>()
53 .addRule({
54 field: "username",
55 validator: (value) => value.length >= 3,
56 message: "Username must be at least 3 characters"
57 })
58 .addRule({
59 field: "email",
60 validator: (value) => value.includes("@"),
61 message: "Email must be valid"
62 })
63 .addRule({
64 field: "password",
65 validator: (value) => value.length >= 8,
66 message: "Password must be at least 8 characters"
67 })
68 .addRule({
69 field: "age",
70 validator: (value) => value >= 13,
71 message: "Must be at least 13 years old"
72 });
73
74const formData: RegisterForm = {
75 username: "jo",
76 email: "invalid-email",
77 password: "short",
78 age: 12
79};
80
81const result = userValidator.validate(formData);
82console.log(result); // Shows all validation errorsGeneric Pipeline Pattern:
1// Functional pipeline with generics
2type PipelineStep<TInput, TOutput> = (input: TInput) => TOutput | Promise<TOutput>;
3
4class Pipeline<T> {
5 constructor(private value: T) {}
6
7 pipe<U>(step: PipelineStep<T, U>): Pipeline<U> {
8 const result = step(this.value);
9 return new Pipeline(result instanceof Promise ?
10 result as any : result);
11 }
12
13 async execute(): Promise<T> {
14 return this.value instanceof Promise ?
15 await this.value : this.value;
16 }
17}
18
19// Usage
20const processData = (data: string) => new Pipeline(data)
21 .pipe(text => text.trim())
22 .pipe(text => text.toLowerCase())
23 .pipe(text => text.split(' '))
24 .pipe(words => words.filter(word => word.length > 2))
25 .pipe(async words => {
26 // Simulate async operation
27 await new Promise(resolve => setTimeout(resolve, 100));
28 return words.map(word => word.charAt(0).toUpperCase() + word.slice(1));
29 })
30 .execute();
31
32// Result: Promise<string[]>Your progress is saved in this browser, on this device only.