TypeScript Patterns That Actually Make Code Better
TypeScript is more than just "JavaScript with types". When used properly, it can prevent entire classes of bugs before you even run your code.
Here are the patterns I use every day that have saved me countless hours of debugging.
1. Discriminated Unions (Tagged Unions)
This is the most powerful pattern in TypeScript.
The Problem
interface SuccessResponse {
data: User[];
}
interface ErrorResponse {
error: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
// ❌ TypeScript doesn't know which one it is
console.log(response.data); // Error: Property 'data' might not exist
}The Solution
interface SuccessResponse {
status: 'success'; // Discriminant property
data: User[];
}
interface ErrorResponse {
status: 'error'; // Discriminant property
error: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
// ✅ TypeScript knows it's SuccessResponse
console.log(response.data); // No error!
} else {
// ✅ TypeScript knows it's ErrorResponse
console.log(response.error); // No error!
}
}Real-world usage in my labs:
type AttackStage =
| { stage: 'idle' }
| { stage: 'scanning'; progress: number }
| { stage: 'exploiting'; target: string; progress: number }
| { stage: 'complete'; results: ExploitResult[] };
function renderAttack(state: AttackStage) {
switch (state.stage) {
case 'idle':
return <IdleState />;
case 'scanning':
return <ScanningState progress={state.progress} />;
case 'exploiting':
return <ExploitingState target={state.target} progress={state.progress} />;
case 'complete':
return <CompleteState results={state.results} />;
}
}No runtime checks needed - TypeScript guarantees the correct properties exist.
2. Branded Types (Nominal Types)
Prevent mixing up similar primitive types.
The Problem
type UserId = string;
type Email = string;
function sendEmail(email: Email) {
// Send email
}
const userId: UserId = "user123";
sendEmail(userId); // ❌ Compiles fine but is wrong!The Solution
type UserId = string & { readonly __brand: 'UserId' };
type Email = string & { readonly __brand: 'Email' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes('@')) throw new Error('Invalid email');
return email as Email;
}
function sendEmail(email: Email) {
// Send email
}
const userId = createUserId("user123");
const email = createEmail("user@example.com");
sendEmail(userId); // ✅ TypeScript error!
sendEmail(email); // ✅ Works!Real-world usage:
type IPv4Address = string & { readonly __brand: 'IPv4' };
type MACAddress = string & { readonly __brand: 'MAC' };
type Hash = string & { readonly __brand: 'Hash' };
function validateIPv4(ip: string): IPv4Address {
const regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!regex.test(ip)) throw new Error('Invalid IPv4');
return ip as IPv4Address;
}3. Template Literal Types
Type-safe string manipulation.
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = '/users' | '/posts' | '/comments';
type Endpoint = `${HTTPMethod} ${Route}`;
// Valid:
const endpoint1: Endpoint = 'GET /users';
const endpoint2: Endpoint = 'POST /comments';
// Invalid:
const endpoint3: Endpoint = 'PATCH /users'; // ✅ Error!
const endpoint4: Endpoint = 'GET /invalid'; // ✅ Error!Real-world usage in my labs:
type LabId = 'red-team' | 'blue-team' | 'purple-team';
type ModuleName = 'nmap' | 'hydra' | 'burpsuite';
type ModulePath = `/${LabId}-lab/${ModuleName}`;
// Generates all valid paths:
// '/red-team-lab/nmap'
// '/blue-team-lab/hydra'
// etc.4. Const Assertions
Make objects deeply readonly and infer literal types.
Without const assertion:
const config = {
api: 'https://api.example.com',
timeout: 5000
};
// Type: { api: string; timeout: number; }With const assertion:
const config = {
api: 'https://api.example.com',
timeout: 5000
} as const;
// Type: { readonly api: "https://api.example.com"; readonly timeout: 5000; }Real-world usage:
const ATTACK_PHASES = [
'reconnaissance',
'weaponization',
'delivery',
'exploitation',
'installation',
'command-and-control',
'actions-on-objectives'
] as const;
type AttackPhase = typeof ATTACK_PHASES[number];
// Type: "reconnaissance" | "weaponization" | ... (exact literals!)
function setPhase(phase: AttackPhase) {
// Only accepts exact phase names
}
setPhase('reconnaissance'); // ✅
setPhase('invalid'); // ✅ Error!5. Utility Types for State Management
Zustand store with proper typing:
interface CybersecurityStore {
moduleProgress: Record<string, ModuleProgress>;
markModuleComplete: (moduleId: string) => void;
resetProgress: () => void;
}
// Extract just the state
type State = Omit<CybersecurityStore, keyof Actions>;
// Extract just the actions
interface Actions {
markModuleComplete: CybersecurityStore['markModuleComplete'];
resetProgress: CybersecurityStore['resetProgress'];
}6. Exhaustiveness Checking
Ensure you handle all cases:
type AlertLevel = 'low' | 'medium' | 'high' | 'critical';
function getAlertColor(level: AlertLevel): string {
switch (level) {
case 'low':
return 'green';
case 'medium':
return 'yellow';
case 'high':
return 'orange';
case 'critical':
return 'red';
default:
// ✅ This ensures all cases are handled
const exhaustiveCheck: never = level;
throw new Error(`Unhandled level: ${exhaustiveCheck}`);
}
}
// If you add a new level, TypeScript will error until you handle it!7. Conditional Types
Types that depend on other types:
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<string>; // falseReal-world usage:
type ApiResponse<T> = T extends void
? { status: 'success' }
: { status: 'success'; data: T } | { status: 'error'; error: string };
// If T is void, only status field exists
const voidResponse: ApiResponse<void> = { status: 'success' };
// Otherwise, data or error exists
const dataResponse: ApiResponse<User[]> = {
status: 'success',
data: []
};8. Type Guards
Runtime type checking with type narrowing:
interface NetworkPacket {
type: 'tcp' | 'udp';
source: string;
destination: string;
}
interface TCPPacket extends NetworkPacket {
type: 'tcp';
flags: string[];
}
interface UDPPacket extends NetworkPacket {
type: 'udp';
length: number;
}
function isTCPPacket(packet: NetworkPacket): packet is TCPPacket {
return packet.type === 'tcp';
}
function handlePacket(packet: NetworkPacket) {
if (isTCPPacket(packet)) {
// ✅ TypeScript knows it's TCPPacket
console.log(packet.flags);
}
}9. Mapped Types
Transform existing types:
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
interface User {
id: string;
name: string;
email: string;
}
type PartialUser = Optional<User>;
// { id?: string; name?: string; email?: string; }
type ReadonlyUser = Readonly<User>;
// { readonly id: string; readonly name: string; readonly email: string; }10. infer Keyword
Extract types from complex types:
type GetArrayType<T> = T extends (infer U)[] ? U : never;
type StringArray = GetArrayType<string[]>; // string
type NumberArray = GetArrayType<number[]>; // numberReal-world usage:
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: '1', name: 'John' };
}
type UserType = GetReturnType<typeof getUser>;
// { id: string; name: string; }Conclusion
These patterns have dramatically reduced bugs in my code:
- No more `undefined is not a function`
- No more accessing properties that don't exist
- No more mixing up similar string types
- Compile-time verification of business logic
TypeScript isn't just about adding types - it's about modeling your domain accurately and letting the compiler work for you.
Using TypeScript effectively? I'd love to hear about your patterns!
James G. - AI Alchemist | Full-Stack Developer