TypeScript Tips and Best Practices
Practical TypeScript tips — ReturnType, discriminated unions, mapped types, readonly, exhaustiveness checks, and predicate type guards
Introduction
TypeScript's type system is incredibly powerful. These practical tips cover patterns that make your types more precise, your code safer, and your development experience better.
ReturnType
Extract the return type of a function automatically:
// Instead of manually typing the return value
function getUser() {
return { id: 1, name: 'Alice', email: 'alice@example.com' };
}
// ✅ Use ReturnType
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }
// Practical: keep return type in sync automatically
function createAction(type: string, payload: unknown) {
return { type, payload, timestamp: Date.now() };
}
type Action = ReturnType<typeof createAction>;
Discriminated Unions
Use literal types as discriminators for type-safe branching:
// ✅ Discriminated union
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'rectangle'; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2; // radius is typed!
case 'square':
return shape.side ** 2; // side is typed!
case 'rectangle':
return shape.width * shape.height; // width/height typed!
}
}
// ❌ Avoid discriminated unions for global action types
// They create coupling across the entire codebase
The in Operator for Type Narrowing
interface User {
name: string;
email: string;
}
interface Admin {
name: string;
email: string;
permissions: string[];
}
function processPerson(person: User | Admin) {
// ✅ Narrow with 'in' operator
if ('permissions' in person) {
console.log(person.permissions); // Admin
} else {
console.log(person.email); // User
}
}
Mapped Types
Transform existing types programmatically:
// Basic mapped type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// More examples
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Index signature
type StringMap = {
[index: string]: string;
};
// Key remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
Deep Readonly
Prevent mutations at all levels of nested objects:
type DeepReadonlyObject<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
};
type DeepReadonly<T> = T extends (infer E)[]
? ReadonlyArray<DeepReadonlyObject<E>>
: T extends object
? DeepReadonlyObject<T>
: T;
// Usage — perfect for Redux state
interface RootState {
user: { name: string; preferences: { theme: string } };
items: { id: number; data: string }[];
}
type ReadonlyRootState = DeepReadonly<RootState>;
// Now all nested properties are readonly
declare const state: ReadonlyRootState;
// state.user.name = 'new'; // Error!
// state.items[0].data = 'new'; // Error!
Exhaustiveness Checks with never
Ensure switch statements handle all cases:
type Status = 'idle' | 'loading' | 'success' | 'error';
function handleStatus(status: Status): string {
switch (status) {
case 'idle':
return 'Ready';
case 'loading':
return 'Loading...';
case 'success':
return 'Done!';
case 'error':
return 'Failed';
default:
// ✅ Exhaustiveness check — compile error if new status added
const _exhaustive: never = status;
return _exhaustive;
}
}
Conditional Types
Create types that depend on conditions:
// Basic conditional type
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
// Extract promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type C = Unwrap<Promise<string>>; // string
// Filter union types
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserStringKeys = StringKeys<User>; // 'name' | 'email'
Predicate Type Guards
Custom type narrowing functions:
// ✅ Predicate type guard
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'name' in obj &&
'email' in obj
);
}
// Usage
const data: unknown = fetchData();
if (isUser(data)) {
console.log(data.name); // TypeScript knows this is User
}
// Array filter with predicate
const items: (User | Admin)[] = getItems();
const users = items.filter(isUser); // User[]
Module Augmentation
Extend existing types from libraries:
// Augment Express Request
declare global {
namespace Express {
interface Request {
user?: { id: string; role: string };
requestId: string;
}
}
}
// Augment existing interfaces
interface Window {
analytics: {
track: (event: string, data?: unknown) => void;
};
}
Best Practices Summary
Do's
// ✅ Use ReturnType for derived types
type Result = ReturnType<typeof myFunction>;
// ✅ Use discriminated unions for variant types
type Response = { status: 'ok'; data: unknown } | { status: 'error'; message: string };
// ✅ Use mapped types for transformations
type Partial<T> = { [K in keyof T]?: T[K] };
// ✅ Use predicate guards for type narrowing
function isError(value: unknown): value is Error { return value instanceof Error; }
// ✅ Use never for exhaustiveness checks
default: const _: never = value;
Don'ts
// ❌ Don't use discriminated unions for global types
// Creates unnecessary coupling
// ❌ Don't over-use conditional types
// Simple types are easier to understand
// ❌ Don't forget readonly for immutable data
// Especially important for Redux/state management
// ❌ Don't use 'any' as a shortcut
// Use 'unknown' and narrow with type guards
// ❌ Don't ignore strict mode
// Enable strict: true in tsconfig.json
Conclusion
These TypeScript patterns make your code safer and more expressive:
- ReturnType — Keep types in sync with implementations
- Discriminated unions — Type-safe branching
- Mapped types — Programmatic type transformations
- Deep Readonly — Prevent accidental mutations
- Predicate guards — Custom type narrowing
- Exhaustiveness checks — Catch missing cases at compile time
Master these and TypeScript becomes a powerful ally rather than a burden.
#typescript #tips #type-system #javascript #web-development