10 TypeScript Tips That Changed How I Write Code
TypeScript has fundamentally changed how I approach software development. Here are 10 tips that made the biggest difference in my code quality.
1. Use const Assertions for Literal Types
Instead of getting broad types, lock them down:
// Without const assertion
const config = {
env: "production",
port: 3000,
}; // type: { env: string, port: number }
// With const assertion
const config = {
env: "production",
port: 3000,
} as const; // type: { readonly env: "production", readonly port: 3000 }
2. Discriminated Unions for State Management
Perfect for handling different states in your application:
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function handleState<T>(state: RequestState<T>) {
switch (state.status) {
case "idle":
return "Ready to fetch";
case "loading":
return "Loading...";
case "success":
return `Got ${state.data}`; // TypeScript knows data exists
case "error":
return `Error: ${state.error.message}`; // TypeScript knows error exists
}
}
Discriminated unions eliminate entire categories of bugs by making impossible states unrepresentable.
3. Template Literal Types
Create precise string types:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/${string}`;
type APIRoute = `${HTTPMethod} ${APIEndpoint}`;
const route: APIRoute = "GET /api/users"; // ✅
const badRoute: APIRoute = "FETCH /api/users"; // ❌ Error!
4. The satisfies Operator
Validate types without widening:
type Colors = Record<string, [number, number, number] | string>;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Colors;
// palette.red is still [number, number, number], not string | [number, number, number]
const redChannel = palette.red[0]; // ✅ Works!
5. Utility Types Are Your Friends
Master these built-in helpers:
interface User {
id: string;
name: string;
email: string;
password: string;
}
// Pick only what you need
type PublicUser = Pick<User, "id" | "name">;
// Omit sensitive fields
type SafeUser = Omit<User, "password">;
// Make everything optional for updates
type UserUpdate = Partial<User>;
// Make everything required
type RequiredUser = Required<User>;
// Read-only version
type FrozenUser = Readonly<User>;
6. Generic Constraints
Don't just use any - constrain your generics:
// Bad
function getProperty<T>(obj: T, key: string): any {
return obj[key];
}
// Good
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Dapo", age: 25 };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
7. Infer Types from Functions
Let TypeScript do the work:
function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age, createdAt: new Date() };
}
// Automatically infer the return type
type User = ReturnType<typeof createUser>;
// { id: string, name: string, age: number, createdAt: Date }
// Get parameter types
type CreateUserParams = Parameters<typeof createUser>;
// [string, number]
8. Branded Types for Type Safety
Prevent mixing up similar types:
type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
function getUser(id: UserId) {
/* ... */
}
const userId = createUserId("user-123");
const postId = createPostId("post-456");
getUser(userId); // ✅
getUser(postId); // ❌ Error: PostId is not assignable to UserId
9. Exhaustive Checks with never
Catch missing cases at compile time:
type Shape = "circle" | "square" | "triangle";
function getArea(shape: Shape): number {
switch (shape) {
case "circle":
return Math.PI * 10 * 10;
case "square":
return 10 * 10;
case "triangle":
return (10 * 10) / 2;
default:
// This will error if we add a new shape but forget to handle it
const _exhaustive: never = shape;
throw new Error(`Unknown shape: ${_exhaustive}`);
}
}
10. Type Guards for Runtime Safety
Bridge the gap between compile-time and runtime:
interface Dog {
bark(): void;
breed: string;
}
interface Cat {
meow(): void;
color: string;
}
// Type guard function
function isDog(pet: Dog | Cat): pet is Dog {
return "bark" in pet;
}
function handlePet(pet: Dog | Cat) {
if (isDog(pet)) {
pet.bark(); // TypeScript knows it's a Dog
console.log(pet.breed);
} else {
pet.meow(); // TypeScript knows it's a Cat
console.log(pet.color);
}
}
Bonus: A TypeScript Haiku
Types flow like water, Catching bugs before they swim, Peace in strict mode's arms.
Want to dive deeper? Check out the TypeScript Handbook and Total TypeScript for advanced patterns.
What's your favorite TypeScript tip? Let me know on Twitter!