TypeScript Tips and Tricks
TypeScript has become the de facto standard for building scalable JavaScript applications. Let’s explore some advanced patterns that will level up your TypeScript game.
Type Guards
Type guards help TypeScript narrow down types within conditional blocks:
interface Bird { fly(): void; layEggs(): void;}
interface Fish { swim(): void; layEggs(): void;}
function isBird(pet: Bird | Fish): pet is Bird { return (pet as Bird).fly !== undefined;}
function move(pet: Bird | Fish) { if (isBird(pet)) { pet.fly(); // TypeScript knows pet is Bird here } else { pet.swim(); // TypeScript knows pet is Fish here }}User-Defined Type Guards
Create custom type guards for complex scenarios:
type Success<T> = { status: 'success'; data: T };type Failure = { status: 'error'; error: string };type Result<T> = Success<T> | Failure;
function isSuccess<T>(result: Result<T>): result is Success<T> { return result.status === 'success';}
async function handleResult<T>(result: Result<T>) { if (isSuccess(result)) { console.log(result.data); // Type-safe access } else { console.error(result.error); // Also type-safe }}Utility Types
TypeScript provides powerful utility types. Here are some essential ones:
Partial and Required
interface User { id: string; name: string; email: string; age: number;}
// Make all properties optionaltype PartialUser = Partial<User>;
// Make all properties requiredtype RequiredUser = Required<PartialUser>;
// Real-world examplefunction updateUser(id: string, updates: Partial<User>) { // Can update any subset of user properties}Pick and Omit
// Pick specific propertiestype UserPreview = Pick<User, 'name' | 'email'>;
// Omit specific propertiestype UserWithoutId = Omit<User, 'id'>;
// Combine with other typestype CreateUserDto = Omit<User, 'id'> & { password: string;};Generic Constraints
Generics become powerful with constraints:
interface HasId { id: string;}
function findById<T extends HasId>(items: T[], id: string): T | undefined { return items.find(item => item.id === id);}
// Usageinterface Product extends HasId { name: string; price: number;}
const products: Product[] = [ { id: '1', name: 'Widget', price: 9.99 }, { id: '2', name: 'Gadget', price: 19.99 },];
const product = findById(products, '1'); // Type: Product | undefinedConditional Types
Create types that depend on conditions:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // truetype B = IsString<42>; // false
// Practical exampletype Flatten<T> = T extends Array<infer U> ? U : T;
type NumberArray = number[];type FlatNumber = Flatten<NumberArray>; // numbertype FlatString = Flatten<string>; // stringMapped Types
Transform existing types into new ones:
type Readonly<T> = { readonly [P in keyof T]: T[P];};
type Nullable<T> = { [P in keyof T]: T[P] | null;};
interface Product { name: string; price: number;}
type ReadonlyProduct = Readonly<Product>;type NullableProduct = Nullable<Product>;// {// name: string | null;// price: number | null;// }Advanced Mapping
// Make all properties mutabletype Mutable<T> = { -readonly [P in keyof T]: T[P];};
// Make all properties required and non-nullabletype Concrete<T> = { [P in keyof T]-?: NonNullable<T[P]>;};Template Literal Types
Build types from string patterns:
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';type Endpoint = '/users' | '/products' | '/orders';
type APIRoute = `${HTTPMethod} ${Endpoint}`;// Result:// "GET /users" | "GET /products" | "GET /orders" |// "POST /users" | "POST /products" | ... etc
// Practical exampletype EventName = 'click' | 'focus' | 'blur';type ElementEventHandler = `on${Capitalize<EventName>}`;// Result: "onClick" | "onFocus" | "onBlur"Discriminated Unions
Create type-safe state machines:
type LoadingState = { status: 'loading';};
type SuccessState<T> = { status: 'success'; data: T;};
type ErrorState = { status: 'error'; error: Error;};
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
function handleState<T>(state: AsyncState<T>) { switch (state.status) { case 'loading': console.log('Loading...'); break; case 'success': console.log('Data:', state.data); // TypeScript knows data exists break; case 'error': console.log('Error:', state.error.message); // TypeScript knows error exists break; }}Type Inference
Let TypeScript do the heavy lifting:
// Infer return typesfunction createUser(name: string, age: number) { return { id: Math.random().toString(), name, age, createdAt: new Date(), };}
type User = ReturnType<typeof createUser>;// {// id: string;// name: string;// age: number;// createdAt: Date;// }
// Infer parameter typestype CreateUserParams = Parameters<typeof createUser>;// [name: string, age: number]Best Practices
Here are some tips for writing better TypeScript:
- Prefer
unknownoverany- Forces type checking - Use
constassertions - Creates more specific types - Leverage discriminated unions - Better than complex conditionals
- Type your errors - Don’t just use
Error - Use
satisfiesoperator - Ensures type compliance without widening
// const assertionconst config = { apiUrl: 'https://api.example.com', timeout: 5000,} as const;
// config.apiUrl is type "https://api.example.com", not string
// satisfies operator (TypeScript 4.9+)type Theme = { primary: string; secondary: string;};
const theme = { primary: '#4d9375', secondary: '#3a7a5e', // accent: 'blue', // Error: not in Theme type} satisfies Theme;Conclusion
TypeScript’s type system is incredibly powerful. These patterns will help you:
- Write more type-safe code
- Catch bugs at compile time
- Improve code documentation
- Enhance IDE autocomplete
Remember: Types are there to help you, not to fight against. Embrace them!
Keep exploring the TypeScript handbook and don’t be afraid to dive into advanced features. Your future self will thank you!