⚡IntermediateTypeScript in Practice
TypeScript Best Practices
Consolidate intermediate-level knowledge with practical patterns: when to use `unknown` over `any`, type-driven development, and structuring TypeScript projects.
Overview
After learning TypeScript's features, the next step is knowing *when* to use them and what patterns lead to maintainable, safe codebases.
The most important meta-principle is **make illegal states unrepresentable** - use the type system to encode invariants so that bugs simply cannot be expressed as valid TypeScript code. Discriminated unions for state, branded types for IDs, and literal types for restricted values all serve this goal.
**Prefer `unknown` over `any`** - `unknown` forces you to narrow before use, which is safe; `any` turns off type checking completely and spreads unsafely. Every `any` is a hole in your type coverage.
**Type-driven development** means writing the types first, then implementing the logic. When the types are right, the implementation almost writes itself - the compiler tells you what's missing.
**Avoid type gymnastics** - if a type gets so complex that it's hard to read, the code it models is probably also too complex. Simplify the design before reaching for advanced type features.
Key Points
- •`unknown` over `any`: `unknown` requires narrowing before use; `any` bypasses all checks
- •Prefer `type` for unions/intersections and computed types; prefer `interface` for object shapes that need merging
- •Make illegal states unrepresentable: encode business rules in types, not just comments
- •`as const` on object literals: `{ status: "active" } as const` gives literal types, not widened types
- •Use `satisfies` to validate an expression against a type without widening it
- •Avoid type assertions (`as`) - if you need `as`, that is often a design smell; use proper narrowing instead
- •Export types separately with `export type` to allow tree-shaking and avoid circular module issues
- •Co-locate types with the code that uses them - don't dump all types in a single `types.ts` file
Examples
`unknown` vs `any` - the critical difference
// ANY - the escape hatch (avoid unless absolutely necessary)
function processAny(value: any): string {
return value.toUpperCase(); // No error - even if value is a number!
// Runtime: TypeError if value.toUpperCase is not a function
}
// UNKNOWN - forces narrowing before use (safe)
function processUnknown(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase(); // OK - narrowed to string
}
if (typeof value === 'number') {
return value.toFixed(2); // OK - narrowed to number
}
return String(value); // Fallback
}
// When to use unknown:
// 1. Function parameters that accept any input (JSON parsing, event handlers)
// 2. Catch clause variables (already the default in strict mode)
// 3. Third-party callback data
// When it's OK to use any:
// 1. Migrating JS to TS incrementally (temporary)
// 2. Mocking in tests where the exact shape doesn't matter
// 3. Library code that uses complex meta-programming patterns
// NEVER: use any to silence a TypeScript error you don't understand
// That's the most common cause of production TypeScript bugsType-driven development - types first
// Step 1: Define the domain types precisely
type OrderStatus = 'draft' | 'pending' | 'paid' | 'shipped' | 'cancelled';
interface Order {
id: string;
status: OrderStatus;
items: Array<{ productId: string; quantity: number; price: number }>;
total: number;
customerId: string;
createdAt: Date;
paidAt?: Date; // only when status === 'paid'
shippedAt?: Date; // only when status === 'shipped'
}
// Step 2: Define what transitions are possible (state machine)
type TransitionMap = {
[K in OrderStatus]?: OrderStatus[];
};
const validTransitions: TransitionMap = {
draft: ['pending', 'cancelled'],
pending: ['paid', 'cancelled'],
paid: ['shipped'],
shipped: [], // terminal
cancelled: [], // terminal
};
// Step 3: Enforce transitions at the type level
function transition(
order: Order,
newStatus: OrderStatus
): Order {
const allowed = validTransitions[order.status] ?? [];
if (!allowed.includes(newStatus)) {
throw new Error(
`Cannot transition from ${order.status} to ${newStatus}`
);
}
return {
...order,
status: newStatus,
paidAt: newStatus === 'paid' ? new Date() : order.paidAt,
shippedAt: newStatus === 'shipped' ? new Date() : order.shippedAt,
};
}Common patterns - `as const`, `satisfies`, export type
// as const - preserves literal types
const ROUTES = {
home: '/',
about: '/about',
contact: '/contact',
} as const;
// Without as const: ROUTES.home is string
// With as const: ROUTES.home is "/"
type Route = typeof ROUTES[keyof typeof ROUTES];
// "/" | "/about" | "/contact"
function navigate(route: Route): void {
window.location.href = route;
}
navigate(ROUTES.about); // OK
// navigate('/other'); // Error - not in the union
// satisfies - validate without widening
const palette = {
primary: '#3b82f6',
secondary: '#8b5cf6',
} satisfies Record<string, string>;
// palette.primary is still "#3b82f6" (literal) not string (widened)
// export type - type-only export (avoids circular deps, enables tree-shaking)
export type { Order, OrderStatus }; // type only
export { transition }; // value + type
// Prefer interface for extensible shapes
interface Plugin {
name: string;
setup(): void;
}
// Prefer type for computed/union types
type ID = string | number;
type NullablePlugin = Plugin | null;Common Mistakes
- Using `any` to silence a TypeScript error - this is almost always a symptom of a type system misunderstanding; spend five minutes understanding the error before reaching for `any`
- Creating one big `types.ts` file for the entire project - types should live close to the code that uses them; co-location makes refactoring easier
- Writing overly complex type utilities when simpler types would work - readability is a virtue; a complex type that's hard to understand is a code smell, not cleverness
Note
The best TypeScript code is code where removing a type annotation reveals an obvious inference - the types document what the code already expresses. When a type is much more complex than the code it describes, reconsider the design rather than the types.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 3 exercises
Knowledge Check
Question 1 of 4