Learn TypeScript Patterns
107 patterns across 16 categories. Each one shows the convention, a side-by-side example, and why it matters.
Start here
New to responsive design? Follow these five categories in order.
Type Narrowing
Discriminated unions, type guards, typeof, and instanceof for safely accessing properties. You'll hit this when TypeScript complains about a property that might not exist on a union type.
function double(value: string | number) {
// No narrowing, just cast
return (value as number) * 2;
}
double("hello"); // NaN at runtimefunction double(value: string | number) {
// No narrowing, just cast
return (value as number) * 2;
}
double("hello"); // NaN at runtimefunction double(value: string | number) {
if (typeof value === "number") {
return value * 2;
}
return value.repeat(2);
}
double("hello"); // "hellohello"function double(value: string | number) {
if (typeof value === "number") {
return value * 2;
}
return value.repeat(2);
}
double("hello"); // "hellohello"Generics
Type parameters, constraints, inference, and when to let TypeScript figure it out. You'll hit this when you write a function that should work with multiple types but you keep reaching for `any`.
function first(arr: any[]): any {
return arr[0];
}
const val = first([1, 2, 3]);
// val is 'any', no autocomplete
val.toFixed(2); // No error even if wrongfunction first(arr: any[]): any {
return arr[0];
}
const val = first([1, 2, 3]);
// val is 'any', no autocomplete
val.toFixed(2); // No error even if wrongfunction first<T>(arr: T[]): T {
return arr[0];
}
const val = first([1, 2, 3]);
// val is 'number', full autocomplete
val.toFixed(2); // OK, checked by compilerfunction first<T>(arr: T[]): T {
return arr[0];
}
const val = first([1, 2, 3]);
// val is 'number', full autocomplete
val.toFixed(2); // OK, checked by compilerUtility Types
Partial, Required, Pick, Omit, Record, and Extract for transforming types without rewriting them. You'll hit this when you need a version of an existing type with some fields optional or removed.
interface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: User) {
// Caller must pass ALL fields, even
// if they only want to update one
}
updateUser("1", {
name: "Alice",
email: "alice@example.com",
age: 30,
}); // Just wanted to update name!interface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: User) {
// Caller must pass ALL fields, even
// if they only want to update one
}
updateUser("1", {
name: "Alice",
email: "alice@example.com",
age: 30,
}); // Just wanted to update name!interface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: Partial<User>) {
// Caller passes only the fields to update
}
updateUser("1", { name: "Alice" }); // OKinterface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: Partial<User>) {
// Caller passes only the fields to update
}
updateUser("1", { name: "Alice" }); // OKUnion & Intersection
Union types for alternatives, intersection types for combining, and the `never` type for exhaustive checks. You'll hit this when a value can be one of several shapes and you need to handle all of them.
// Overly broad type
function setStatus(status: string) {
// Any string is accepted
}
setStatus("active"); // OK
setStatus("acitve"); // No error, typo undetected
setStatus("banana"); // No error, nonsense value// Overly broad type
function setStatus(status: string) {
// Any string is accepted
}
setStatus("active"); // OK
setStatus("acitve"); // No error, typo undetected
setStatus("banana"); // No error, nonsense valuetype Status = "active" | "inactive" | "pending";
function setStatus(status: Status) {
// Only valid values accepted
}
setStatus("active"); // OK
setStatus("acitve"); // Error: typo caught
setStatus("banana"); // Error: not a valid Statustype Status = "active" | "inactive" | "pending";
function setStatus(status: Status) {
// Only valid values accepted
}
setStatus("active"); // OK
setStatus("acitve"); // Error: typo caught
setStatus("banana"); // Error: not a valid StatusType Assertions
as const, satisfies, type predicates, and why `as` should be your last resort. You'll hit this when you know more than the compiler but want to prove it safely instead of just overriding.
type Color = "red" | "green" | "blue";
const palette = {
primary: "red",
secondary: "green",
} as Record<string, Color>;
// Lost the specific keys!
palette.primary; // Color (not "red")
palette.oops; // Color (no error for bad key)type Color = "red" | "green" | "blue";
const palette = {
primary: "red",
secondary: "green",
} as Record<string, Color>;
// Lost the specific keys!
palette.primary; // Color (not "red")
palette.oops; // Color (no error for bad key)type Color = "red" | "green" | "blue";
const palette = {
primary: "red",
secondary: "green",
} satisfies Record<string, Color>;
// Keeps specific keys and values!
palette.primary; // "red"
palette.oops; // Error: property doesn't existtype Color = "red" | "green" | "blue";
const palette = {
primary: "red",
secondary: "green",
} satisfies Record<string, Color>;
// Keeps specific keys and values!
palette.primary; // "red"
palette.oops; // Error: property doesn't existEnums & Literals
String literal unions vs enums, const assertions, and when numeric enums cause trouble. You'll hit this when you need a fixed set of values and can't decide between `type Status = 'active' | 'inactive'` and `enum Status`.
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
// Must import enum to use values
// Generates runtime JavaScript object
// JSON requires conversion: Color[json.color]
function paint(color: Color) { }
paint(Color.Red);enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
// Must import enum to use values
// Generates runtime JavaScript object
// JSON requires conversion: Color[json.color]
function paint(color: Color) { }
paint(Color.Red);type Color = "RED" | "GREEN" | "BLUE";
// No import needed for values
// Zero runtime cost (erased at compile time)
// JSON strings work directly
function paint(color: Color) { }
paint("RED");type Color = "RED" | "GREEN" | "BLUE";
// No import needed for values
// Zero runtime cost (erased at compile time)
// JSON strings work directly
function paint(color: Color) { }
paint("RED");Strict Mode
strictNullChecks, noUncheckedIndexedAccess, exactOptionalPropertyTypes, and the flags that catch real bugs. You'll hit this when your code compiles fine but crashes at runtime because a value was unexpectedly undefined.
// tsconfig: "strict": false
function getLength(name: string) {
return name.length;
}
const user = users.find((u) => u.id === id);
// No error, but user might be undefined
console.log(getLength(user.name));
// Runtime: Cannot read property 'name'
// of undefined// tsconfig: "strict": false
function getLength(name: string) {
return name.length;
}
const user = users.find((u) => u.id === id);
// No error, but user might be undefined
console.log(getLength(user.name));
// Runtime: Cannot read property 'name'
// of undefined// tsconfig: "strict": true
function getLength(name: string) {
return name.length;
}
const user = users.find((u) => u.id === id);
// Error: 'user' is possibly 'undefined'
if (user) {
console.log(getLength(user.name));
}// tsconfig: "strict": true
function getLength(name: string) {
return name.length;
}
const user = users.find((u) => u.id === id);
// Error: 'user' is possibly 'undefined'
if (user) {
console.log(getLength(user.name));
}Readonly & Immutability
readonly properties, Readonly<T>, ReadonlyArray, and as const for preventing accidental mutations. You'll hit this when a function modifies an array or object it was only supposed to read.
interface Config {
apiUrl: string;
timeout: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
};
// Oops, accidentally mutated config
config.timeout = -1;
// No error, invalid stateinterface Config {
apiUrl: string;
timeout: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
};
// Oops, accidentally mutated config
config.timeout = -1;
// No error, invalid stateinterface Config {
readonly apiUrl: string;
readonly timeout: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
};
// config.timeout = -1;
// Error: Cannot assign to 'timeout'
// because it is a read-only propertyinterface Config {
readonly apiUrl: string;
readonly timeout: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
};
// config.timeout = -1;
// Error: Cannot assign to 'timeout'
// because it is a read-only propertyFunction Types
Overloads, generic functions, callback typing, and return type inference. You'll hit this when a function should accept different argument shapes and return different types accordingly.
// Callback typed as Function
function fetchData(callback: Function) {
const data = { name: "Alice" };
callback(data);
}
// No type safety on the callback
fetchData((data) => {
// data is 'any'
console.log(data.nonexistent); // No error
});// Callback typed as Function
function fetchData(callback: Function) {
const data = { name: "Alice" };
callback(data);
}
// No type safety on the callback
fetchData((data) => {
// data is 'any'
console.log(data.nonexistent); // No error
});interface User {
name: string;
}
function fetchData(callback: (data: User) => void) {
const data: User = { name: "Alice" };
callback(data);
}
fetchData((data) => {
console.log(data.name); // OK
console.log(data.nonexistent); // Error
});interface User {
name: string;
}
function fetchData(callback: (data: User) => void) {
const data: User = { name: "Alice" };
callback(data);
}
fetchData((data) => {
console.log(data.name); // OK
console.log(data.nonexistent); // Error
});Interface vs Type
When to use interface, when to use type, declaration merging, and extending vs intersecting. You'll hit this when you're unsure which to pick and whether it actually matters.
type User = {
name: string;
role: string;
};
// Intersection: silently merges conflicts
type AdminUser = User & {
role: number; // No error here!
};
// role is 'string & number' which is 'never'
// You only find out when you try to use it
const admin: AdminUser = {
name: "Alice",
role: "admin", // Error: never
};type User = {
name: string;
role: string;
};
// Intersection: silently merges conflicts
type AdminUser = User & {
role: number; // No error here!
};
// role is 'string & number' which is 'never'
// You only find out when you try to use it
const admin: AdminUser = {
name: "Alice",
role: "admin", // Error: never
};interface User {
name: string;
role: string;
}
// Extends: catches conflicts immediately
// interface AdminUser extends User {
// role: number; // Error: not assignable
// }
interface AdminUser extends User {
permissions: string[];
}
const admin: AdminUser = {
name: "Alice",
role: "admin", // OK
permissions: ["read"],
};interface User {
name: string;
role: string;
}
// Extends: catches conflicts immediately
// interface AdminUser extends User {
// role: number; // Error: not assignable
// }
interface AdminUser extends User {
permissions: string[];
}
const admin: AdminUser = {
name: "Alice",
role: "admin", // OK
permissions: ["read"],
};Mapped Types
Key remapping, conditional types, infer keyword, and building types from other types. You'll hit this when you need to transform every property of an existing type in a systematic way.
interface User {
name: string;
email: string;
age: number;
}
// Manually creating an optional version
interface PartialUser {
name?: string;
email?: string;
age?: number;
}
// Must update both when User changesinterface User {
name: string;
email: string;
age: number;
}
// Manually creating an optional version
interface PartialUser {
name?: string;
email?: string;
age?: number;
}
// Must update both when User changesinterface User {
name: string;
email: string;
age: number;
}
// Mapped type transforms every property
type PartialUser = {
[K in keyof User]?: User[K];
};
// Or simply use the built-in:
type AlsoPartialUser = Partial<User>;
// Both auto-update when User changesinterface User {
name: string;
email: string;
age: number;
}
// Mapped type transforms every property
type PartialUser = {
[K in keyof User]?: User[K];
};
// Or simply use the built-in:
type AlsoPartialUser = Partial<User>;
// Both auto-update when User changesTemplate Literals
Template literal types, string manipulation types, and pattern matching on string shapes. You'll hit this when you want the compiler to enforce that a string follows a specific format like `on${string}`.
type EventHandler = {
on: (event: string, cb: () => void) => void;
};
const emitter: EventHandler = getEmitter();
emitter.on("clck", handleClick);
// Typo not caught, handler never fires
// No error at compile timetype EventHandler = {
on: (event: string, cb: () => void) => void;
};
const emitter: EventHandler = getEmitter();
emitter.on("clck", handleClick);
// Typo not caught, handler never fires
// No error at compile timetype EventName =
| `on${"Click" | "Hover" | "Focus"}`
| `on${"Key" | "Mouse"}${"Down" | "Up"}`;
type EventHandler = {
on: (event: EventName, cb: () => void) => void;
};
const emitter: EventHandler = getEmitter();
emitter.on("onClick", handleClick);
// emitter.on("onClck", handleClick);
// Error: not assignable to EventNametype EventName =
| `on${"Click" | "Hover" | "Focus"}`
| `on${"Key" | "Mouse"}${"Down" | "Up"}`;
type EventHandler = {
on: (event: EventName, cb: () => void) => void;
};
const emitter: EventHandler = getEmitter();
emitter.on("onClick", handleClick);
// emitter.on("onClck", handleClick);
// Error: not assignable to EventNameReact + TypeScript
Component props, event handlers, refs, generic components, and polymorphic patterns. You'll hit this when you try to type a React component that forwards refs or accepts an `as` prop.
function UserCard(props: any) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.emial}</p>
{/* Typo: "emial" instead of "email" */}
{/* No error because props is any */}
</div>
);
}
<UserCard name="Alice" />
// Missing email, no errorfunction UserCard(props: any) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.emial}</p>
{/* Typo: "emial" instead of "email" */}
{/* No error because props is any */}
</div>
);
}
<UserCard name="Alice" />
// Missing email, no errorinterface UserCardProps {
name: string;
email: string;
role?: "admin" | "user";
}
function UserCard({ name, email, role = "user" }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<span>{role}</span>
</div>
);
}
// <UserCard name="Alice" />
// Error: missing required prop 'email'interface UserCardProps {
name: string;
email: string;
role?: "admin" | "user";
}
function UserCard({ name, email, role = "user" }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<span>{role}</span>
</div>
);
}
// <UserCard name="Alice" />
// Error: missing required prop 'email'Module Types
Declaration files, ambient modules, type augmentation, and global types. You'll hit this when you import a JavaScript library that has no types or need to extend an existing module's types.
// utils.js (no types)
export function slugify(text) {
return text
.toLowerCase()
.replace(/\s+/g, "-");
}
// app.ts
import { slugify } from "./utils";
// Error: Could not find a declaration
// file for module './utils'
// slugify is implicitly 'any'// utils.js (no types)
export function slugify(text) {
return text
.toLowerCase()
.replace(/\s+/g, "-");
}
// app.ts
import { slugify } from "./utils";
// Error: Could not find a declaration
// file for module './utils'
// slugify is implicitly 'any'// utils.d.ts
export declare function slugify(
text: string
): string;
// Or even better, rename to utils.ts:
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/\s+/g, "-");
}
// app.ts
import { slugify } from "./utils";
// slugify is typed: (text: string) => string// utils.d.ts
export declare function slugify(
text: string
): string;
// Or even better, rename to utils.ts:
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/\s+/g, "-");
}
// app.ts
import { slugify } from "./utils";
// slugify is typed: (text: string) => stringError Handling
Unknown vs any in catch blocks, Result types, type-safe error handling, and assertion functions. You'll hit this when your catch block uses `error.message` and TypeScript says `error` is `unknown`.
try {
await fetchData();
} catch (error) {
// With useUnknownInCatchVariables: false
// error is 'any'
console.log(error.message);
// Works for Error objects but crashes
// if a string or number was thrown
showToast(error.message);
}try {
await fetchData();
} catch (error) {
// With useUnknownInCatchVariables: false
// error is 'any'
console.log(error.message);
// Works for Error objects but crashes
// if a string or number was thrown
showToast(error.message);
}try {
await fetchData();
} catch (error) {
// error is 'unknown'
if (error instanceof Error) {
console.log(error.message);
showToast(error.message);
} else {
console.log("Unknown error:", error);
showToast("An unexpected error occurred");
}
}try {
await fetchData();
} catch (error) {
// error is 'unknown'
if (error instanceof Error) {
console.log(error.message);
showToast(error.message);
} else {
console.log("Unknown error:", error);
showToast("An unexpected error occurred");
}
}Common Mistakes
Overusing any, unnecessary type assertions, ignoring strict flags, and other TypeScript anti-patterns that compile but break. You'll hit this when TypeScript stops catching bugs it should have caught.
function processData(data: any) {
// No errors, but no safety either
data.forEach((item: any) => {
console.log(item.naem); // Typo
updateRecord(item.id.toFixed()); // id might not be number
});
}
// any spreads: anything derived from
// an any value is also anyfunction processData(data: any) {
// No errors, but no safety either
data.forEach((item: any) => {
console.log(item.naem); // Typo
updateRecord(item.id.toFixed()); // id might not be number
});
}
// any spreads: anything derived from
// an any value is also anyinterface DataRecord {
id: number;
name: string;
status: "active" | "inactive";
}
function processData(data: DataRecord[]) {
data.forEach((item) => {
console.log(item.name); // Autocomplete
// item.naem; // Error: typo caught
updateRecord(item.id.toFixed());
});
}interface DataRecord {
id: number;
name: string;
status: "active" | "inactive";
}
function processData(data: DataRecord[]) {
data.forEach((item) => {
console.log(item.name); // Autocomplete
// item.naem; // Error: typo caught
updateRecord(item.id.toFixed());
});
}