Mastering TypeScript: Advanced Patterns and Best Practices

Mastering TypeScript: Advanced Patterns and Best Practices
TypeScript has become the go-to language for building large-scale JavaScript applications. In this post, we'll explore advanced patterns and best practices that will help you write more maintainable code.
Generic Types
Generics are one of TypeScript's most powerful features. Here's how to use them effectively:
interface Repository<T> {
find(id: string): Promise<T>;
save(item: T): Promise<void>;
update(id: string, item: Partial<T>): Promise<void>;
delete(id: string): Promise<void>;
}
// Implementation example
class UserRepository implements Repository<User> {
async find(id: string): Promise<User> {
// Implementation
}
// ... other methods
}
Utility Types
TypeScript provides several utility types that can make your code more expressive:
interface User {
id: string;
name: string;
email: string;
password: string;
}
// Only pick certain properties
type UserDTO = Pick<User, 'id' | 'name' | 'email'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
Type Guards
Type guards help you narrow down types in a type-safe way:
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;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows pet is Fish
} else {
pet.fly(); // TypeScript knows pet is Bird
}
}
Conditional Types
Conditional types allow you to create more flexible type definitions:
type IsString<T> = T extends string ? true : false;
// Examples
type A = IsString<string>; // true
type B = IsString<number>; // false
// More practical example
type ArrayOrSingle<T> = T extends any[] ? T : T[];
function ensureArray<T>(value: T): ArrayOrSingle<T> {
return Array.isArray(value) ? value : [value] as ArrayOrSingle<T>;
}
Mapped Types
Mapped types help you transform existing types into new ones:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
// Example usage
interface Config {
endpoint: string;
apiKey: string;
timeout: number;
}
type ReadonlyConfig = Readonly<Config>;
type OptionalConfig = Optional<Config>;
Best Practices
-
Use strict mode
{ "compilerOptions": { "strict": true } }
-
Prefer interfaces over type aliases for object types
// Good interface User { name: string; age: number; } // Less ideal for objects type User = { name: string; age: number; };
-
Use const assertions for literal types
const config = { endpoint: "api.example.com", method: "POST" } as const;
-
Leverage discriminated unions
interface Success { type: "success"; data: unknown; } interface Error { type: "error"; message: string; } type Result = Success | Error;
Conclusion
TypeScript's type system is incredibly powerful when used correctly. By understanding and applying these advanced patterns, you can write more maintainable and type-safe code. Remember that types are not just for catching errors - they're also documentation for your code and tools for better developer experience.