TypeScript's type system provides powerful features for building type-safe applications. Let's dive deep into generics and advanced types to enhance your TypeScript development skills.
Understanding Generics 🔄
Generics allow you to write flexible, reusable code while maintaining type safety. They act as type variables that can be used across functions, classes, and interfaces.
Basic Generic Functions
function identity<T>(arg: T): T {
return arg;
}
// Usage
const numberResult = identity<number>(42);
const stringResult = identity("Hello"); // Type inference
Generic Interfaces and Classes
interface Container<T> {
value: T;
getValue(): T;
}
class Box<T> implements Container<T> {
constructor(public value: T) {}
getValue(): T {
return this.value;
}
}
// Usage
const numberBox = new Box<number>(123);
const stringBox = new Box("Hello"); // Type inference
Generic Constraints 🔒
Constrain generic types to ensure they have specific properties or methods:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): number {
return arg.length;
}
// Valid usage
logLength("Hello"); // String has length property
logLength([1, 2, 3]); // Array has length property
// logLength(123); // Error: Number doesn't have length property
Multiple Type Parameters
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const result = merge(
{ name: "John" },
{ age: 30 }
); // { name: string; age: number; }
Advanced Type Features ⚡
Conditional Types
type IsString<T> = T extends string ? true : false;
// Usage
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
// More complex example
type ArrayOrSingle<T> = T extends any[] ? T[number] : T;
type Item1 = ArrayOrSingle<string[]>; // string
type Item2 = ArrayOrSingle<number>; // number
Mapped Types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
Template Literal Types
type EventName<T extends string> = `${T}Changed`;
type Direction = "top" | "right" | "bottom" | "left";
type Position = `${Direction}-${number}`;
// Usage
let eventName: EventName<"user"> = "userChanged";
let position: Position = "top-10";
Utility Types 🛠️
Pick and Omit
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
type TodoInfo = Omit<Todo, "completed" | "createdAt">;
// Usage
const todo: TodoPreview = {
title: "Clean room",
completed: false
};
Record Type
type PageInfo = {
title: string;
url: string;
};
type Pages = Record<string, PageInfo>;
const pages: Pages = {
home: { title: "Home", url: "/" },
about: { title: "About", url: "/about" }
};
Partial and Required
interface Config {
host: string;
port: number;
timeout?: number;
}
type PartialConfig = Partial<Config>; // All properties optional
type RequiredConfig = Required<Config>; // All properties required
function updateConfig(config: Partial<Config>) {
// Update only provided properties
}
Advanced Type Inference 🎯
Infer Keyword
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Usage
function getData() {
return { id: 1, name: "John" };
}
type DataType = ReturnType<typeof getData>; // { id: number; name: string; }
Type Guards
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// Usage
function moveAnimal(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows pet is Fish
} else {
pet.fly(); // TypeScript knows pet is Bird
}
}
Practical Examples 💡
Generic API Client
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
class ApiClient {
async get<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return await response.json();
}
async post<T, U>(url: string, data: T): Promise<ApiResponse<U>> {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(data)
});
return await response.json();
}
}
// Usage
interface User {
id: number;
name: string;
}
const api = new ApiClient();
const user = await api.get<User>('/api/user/1');
State Management
type Reducer<S, A> = (state: S, action: A) => S;
interface Action<T = any> {
type: string;
payload?: T;
}
function createStore<S, A extends Action>(
reducer: Reducer<S, A>,
initialState: S
) {
let state = initialState;
return {
getState: () => state,
dispatch: (action: A) => {
state = reducer(state, action);
}
};
}
// Usage
interface CounterState {
count: number;
}
type CounterAction =
| { type: "INCREMENT" }
| { type: "DECREMENT" }
| { type: "SET"; payload: number };
const counterReducer: Reducer<CounterState, CounterAction> = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
case "SET":
return { count: action.payload };
default:
return state;
}
};
const store = createStore(counterReducer, { count: 0 });
Best Practices 📝
- Type Inference
// Let TypeScript infer types when possible
const numbers = [1, 2, 3]; // number[]
const [first, ...rest] = numbers; // first: number, rest: number[]
- Discriminated Unions
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "square":
return shape.size * shape.size;
case "rectangle":
return shape.width * shape.height;
}
}
- Generic Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const obj = { a: 1, b: 2, c: 3 };
getProperty(obj, "a"); // OK
// getProperty(obj, "d"); // Error: "d" is not in keyof T
Conclusion
TypeScript's generics and advanced types provide powerful tools for building type-safe applications. Remember to:
- Use generics for reusable, type-safe code
- Leverage conditional types for complex type relationships
- Utilize utility types for common transformations
- Implement type guards for runtime type checking
- Follow TypeScript best practices
- Take advantage of type inference
These features will help you write more maintainable and robust TypeScript applications.