TypeScript Tips That Make You More Productive
Stop fighting TypeScript. Make it work for you.
1. Type vs Interface (When to Use Which)
// Use interface for object shapes
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
}
// Use type for unions, tuples, computed types
type ID = string | number;
type Status = 'pending' | 'active' | 'archived';
type Coordinates = [number, number]; // Tuple
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'role'
// They can extend each other!
interface AdminUser extends User {
permissions: string[];
lastLogin: Date;
}
type EnhancedUser = User & {
createdAt: Date;
avatar?: string;
};
2. Utility Types (Built-in Type Transformers)
interface Todo {
id: number;
title: string;
description: string;
completed: boolean;
createdAt: Date;
}
// Partial — all fields optional
function updateTodo(id: number, fields: Partial<Todo>) {
// Only update the provided fields
}
updateTodo(1, { completed: true }); // Only need to pass what's changing
// Required — make all fields required
type StrictTodo = Required<Todo>;
// Omit — remove specific keys
type TodoPreview = Omit<Todo, 'description' | 'createdAt'>;
// Pick — keep only specific keys
type TodoSummary = Pick<Todo, 'id' | 'title' | 'completed'>;
// Readonly — immutable version
const initialTodo: Readonly<Todo> = { ... };
initialTodo.completed = true; // Error! Cannot assign
// Record — key-value map type
const userRoles: Record<string, string> = {
admin: 'full_access',
editor: 'content_write',
viewer: 'read_only',
};
// Extract — extract type from a complex type
type TodoTitle = Todo['title']; // string
3. Generic Functions (Reusable Logic)
// Basic generic
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // number | undefined
first(['a', 'b']); // string | undefined
// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getProperty({ name: 'Alex', age: 30 }, 'name'); // string
getProperty({ name: 'Alex', age: 30 }, 'age'); // number
getProperty({ name: 'Alex', age: 30 }, 'xyz'); // Error! 'xyz' not a key
// Generic with multiple constraints
interface HasId { id: string; }
interface HasTimestamp { createdAt: Date; }
function logEntity<T extends HasId & HasTimestamp>(entity: T): void {
console.log(`Entity ${entity.id} created at ${entity.createdAt}`);
}
4. Discriminated Unions (Type-Safe State)
// The pattern that makes TypeScript shine:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleState(state: RequestState<User>): string {
switch (state.status) {
case 'idle':
return 'Not started';
case 'loading':
return 'Loading...';
case 'success':
return `Loaded: ${state.data.name}`; // TS knows data exists here!
case 'error':
return `Error: ${state.error.message}`; // TS knows error exists here!
default:
const _exhaustive: never = state; // Catch missing cases at compile time!
return _exhaustive;
}
}
5. Type Guards (Narrowing Types)
// typeof guard
function printId(id: string | number) {
if (typeof id === 'string') {
console.log(id.toUpperCase()); // TS knows it's string here
} else {
console.log(id.toFixed(2)); // TS knows it's number here
}
}
// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }
function speak(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // TS knows it's Dog
} else {
pet.meow(); // TS knows it's Cat
}
}
// Custom type guard (isX pattern)
interface ApiResponse<T> {
success: true;
data: T;
}
interface ApiError {
success: false;
error: { code: string; message: string };
}
type Result<T> = ApiResponse<T> | ApiError;
function isSuccess<T>(result: Result<T>): result is ApiResponse<T> {
return result.success === true;
}
function handleResult(result: Result<User>) {
if (isSuccess(result)) {
console.log(result.data.name); // TS knows data is available!
} else {
console.log(result.error.message); // TS knows error is available!
}
}
6. satisfies Operator (Validate Without Changing Type)
// Before satisfies: `as const` makes everything readonly/literal
const config = {
api: 'https://api.example.com',
port: 3000,
debug: false,
} as const;
// config.port is now number (literal 3000), not assignable to where number expected
// With satisfies: validates shape but keeps inferred type
const config = {
api: 'https://api.example.com',
port: 3000,
debug: false,
} satisfies Record<string, string | number | boolean>;
// Validated! And port is still just `number`, not literal `3000`
7. Template Literal Types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIRoute = `/api/${string}`;
type Endpoint = `${HTTPMethod} ${APIRoute}`;
// "GET /api/users" | "POST /api/login" etc.
// Practical: CSS property validation
type CSSProperty = `--${string}`;
// Event name patterns
type DOMEvent = `on${Capitalize<string>}`;
// "onClick" | "onChange" | "onSubmit"
8. const Assertions and as const
// Regular object inference (types are widened)
const colors = ['red', 'green', 'blue'];
// Type: string[] (lost the specific values!)
// const assertion
const COLORS = ['red', 'green', 'blue'] as const;
// Type: readonly ['red', 'green', 'blue'] (values preserved!)
// Use in function parameters
function setColor(color: typeof COLORS[number]) {
// color can only be 'red', 'green', or 'blue'
}
setColor('red'); // OK
setColor('yellow'); // Error!
// Object as const
const ROUTES = {
home: '/',
users: '/users',
userProfile: '/users/:id',
} as const;
// Type: { readonly home: '/'; readonly users: '/users'; readonly userProfile: '/users/:id' }
type RouteKey = keyof ROUTES; // 'home' | 'users' | 'userProfile'
9. Error Handling Pattern
// Wrap async functions to return [data, error] tuple instead of throwing
async function tryCatch<T>(promise: Promise<T>): Promise<[T?, Error?]> {
try {
const data = await promise;
return [data, undefined];
} catch (error) {
return [undefined, error as Error];
}
}
// Usage:
const [user, error] = await tryCatch(fetchUser(userId));
if (error) {
console.error('Failed:', error);
return;
}
console.log(user.name); // TS knows user is defined here
10. tsconfig.json Essentials
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true, // Enable all strict checks
"noUncheckedIndexedAccess": true, // array[i] could be undefined
"noImplicitReturns": true, // All paths must return value
"noFallthroughCasesInSwitch": true,// Prevent switch fallthrough
"skipLibCheck": true, // Skip type checking .d.ts files
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"declaration": true, // Generate .d.ts files
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
},
"lib": ["ES2022", "DOM", "DOM.Iterable"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
What's your favorite TypeScript tip? Share it below!
Follow @armorbreak for more developer content.
Top comments (0)