I've been writing TypeScript professionally since 2021 when I joined SparkSoft Solutions, and in the years since, I've seen every flavor of TypeScript code — from beautifully typed APIs to the dreaded 'any' soup that makes you question why you're using TypeScript at all. This post distills the patterns I've found most valuable across dozens of production projects.
Stop Using 'any' — Yes, Even There
The most common TypeScript anti-pattern I encounter in code reviews is the liberal use of 'any'. I get it — sometimes the types are complex and you just want to ship. But every 'any' is a hole in your type safety net, and bugs love to crawl through holes. The fix isn't always adding a complex generic type. Sometimes it's 'unknown' — TypeScript's safe counterpart to 'any' that forces you to narrow the type before using it.
// ❌ The "any" escape hatch
function processData(data: any) {
return data.users.map((u: any) => u.name);
}
// ✅ Type-safe with unknown + narrowing
interface UserResponse {
users: { name: string; email: string }[];
}
function isUserResponse(data: unknown): data is UserResponse {
return (
typeof data === 'object' &&
data !== null &&
'users' in data &&
Array.isArray((data as UserResponse).users)
);
}
function processData(data: unknown) {
if (!isUserResponse(data)) {
throw new Error('Invalid data format');
}
return data.users.map((u) => u.name); // Fully typed!
}Discriminated Unions Are Your Best Friend
If I had to pick one TypeScript pattern that's improved my code the most, it's discriminated unions. They're perfect for modeling state machines, API responses, and any scenario where a value can be one of several shapes. The TypeScript compiler becomes your co-pilot — it will tell you when you've forgotten to handle a case.
// Model API states explicitly
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function renderState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'idle':
return null;
case 'loading':
return <Spinner />;
case 'success':
return <DataView data={state.data} />;
case 'error':
return <ErrorView error={state.error} />;
// TypeScript ensures exhaustive checking
}
}Generic Constraints — Not Just for Library Authors
Many developers think generics are only useful when building libraries or utility types. But constrained generics are incredibly powerful for everyday application code. When I was building the data visualization tools at Camber Morris, generic constraints allowed us to write one reusable chart component that worked safely with any data shape — as long as it had the required numeric fields.
// A reusable function that works with any object
// that has numeric values
type NumericKeys<T> = {
[K in keyof T]: T[K] extends number ? K : never;
}[keyof T];
function sumByField<T>(
items: T[],
field: NumericKeys<T>
): number {
return items.reduce(
(sum, item) => sum + (item[field] as number),
0
);
}
// TypeScript only allows numeric fields
const total = sumByField(orders, 'amount'); // ✅
const bad = sumByField(orders, 'customerName'); // ❌ Error!The 'as const' Trick
One small keyword that dramatically improves type inference: 'as const'. Use it on object literals and arrays to get the narrowest possible type. It's especially powerful for configuration objects, route definitions, and anywhere you want TypeScript to treat your values as literal types rather than their widened counterparts.
Wrapping Up
Clean TypeScript isn't about writing the most clever types — it's about making your code's intent clear to both the compiler and your future self. Start with strict mode enabled, avoid 'any' like the plague, model your domain with discriminated unions, and let the type system do the heavy lifting. Your teammates (and your 2 AM debugging self) will thank you.