Write better TypeScript — utility types, strict mode, discriminated unions, type guards, and patterns that eliminate runtime errors.
TypeScript adds a powerful type system to JavaScript — but it's easy to use it superficially while still writing code that breaks at runtime. These are the patterns that make TypeScript genuinely safer.
// tsconfig.json — start with this
{
"compilerOptions": {
"strict": true, // enables all strict checks
"noUncheckedIndexedAccess": true, // arr[i] is T | undefined
"exactOptionalPropertyTypes": true, // {a?: string} ≠ {a: string | undefined}
"noImplicitReturns": true, // all code paths must return
"noFallthroughCasesInSwitch": true
}
}
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Partial — all properties optional
type UserUpdate = Partial<User>;
// { id?: string; name?: string; ... }
// Required — all properties required
type FullUser = Required<Partial<User>>;
// Pick — select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string }
// Omit — exclude specific properties
type CreateUser = Omit<User, 'id' | 'createdAt'>;
// { name: string; email: string; role: ... }
// Readonly — prevent mutation
type ImmutableUser = Readonly<User>;
// Record — typed object/map
type RoleMap = Record<'admin' | 'user', string[]>;
// ReturnType — extract function return type
type GetUserFn = () => Promise<User>;
type GetUserResult = Awaited<ReturnType<GetUserFn>>; // User
// Parameters — extract function parameter types
type FetchParams = Parameters<typeof fetch>; // [input: RequestInfo, init?: RequestInit]
// ✗ Hard to work with — what's available when?
interface ApiResponse {
data?: User;
error?: string;
loading?: boolean;
}
// ✓ Discriminated union — exhaustive and explicit
type ApiResponse =
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: string };
function render(state: ApiResponse) {
switch (state.status) {
case 'loading': return <Spinner />;
case 'success': return <UserCard user={state.data} />; // data is User here
case 'error': return <Error msg={state.error} />; // error is string here
}
// TypeScript knows this is exhaustive
}
// typeof guard
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // string here
}
return value.toFixed(2); // number here
}
// instanceof guard
function handleError(err: unknown) {
if (err instanceof Error) {
console.log(err.message); // Error here
}
}
// Custom type guard
interface Cat { meow(): void }
interface Dog { bark(): void }
function isCat(animal: Cat | Dog): animal is Cat {
return 'meow' in animal;
}
// in narrowing
function handleShape(shape: Circle | Square) {
if ('radius' in shape) {
return Math.PI * shape.radius ** 2; // Circle
}
return shape.side ** 2; // Square
}
// Build string types at compile time
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts';
type Route = `${Method} ${Endpoint}`;
// 'GET /users' | 'GET /posts' | 'POST /users' | ...
// Event naming patterns
type EventName<T extends string> = `on${Capitalize<T>}`;
type UserEvents = EventName<'click' | 'focus' | 'blur'>;
// 'onClick' | 'onFocus' | 'onBlur'
// CSS property types
type CSSValue = `${number}px` | `${number}rem` | `${number}%`;
any — Use unknown Instead// ✗ any — disables all type checking
function parse(data: any) {
return data.user.name; // no error even if wrong
}
// ✓ unknown — forces you to narrow before using
function parse(data: unknown) {
if (typeof data === 'object' && data !== null && 'user' in data) {
// now TypeScript knows data has 'user'
}
}
// ✓ Or validate with Zod (recommended for API data)
import { z } from 'zod';
const UserSchema = z.object({ name: z.string(), id: z.number() });
type User = z.infer<typeof UserSchema>;
const parsed = UserSchema.parse(apiResponse); // throws if invalid
const safe = UserSchema.safeParse(apiResponse); // returns { success, data/error }
// const assertion — narrow to literal types
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = (typeof ROLES)[number]; // 'admin' | 'user' | 'guest'
// Object const assertion
const CONFIG = {
endpoint: 'https://api.heolab.com',
timeout: 5000,
} as const;
type Endpoint = typeof CONFIG.endpoint; // 'https://api.heolab.com'
// Prefer const over enum for most cases
// enum has runtime footprint; const doesn't
const Direction = { Up: 'UP', Down: 'DOWN' } as const;
type Direction = (typeof Direction)[keyof typeof Direction]; // 'UP' | 'DOWN'
Use the JSON to TypeScript tool to instantly generate TypeScript interfaces from any JSON structure.