TypeScript Best Practices in 2025: Write Better, Safer Code
Master TypeScript with proven best practices for type safety, code organization, and maintainability. Learn advanced patterns, utility types, and real-world techniques from production codebases.
TypeScript has become the standard for serious JavaScript development. After using it across multiple production applications like InsureSignal and Liquidity Hunters, here are the best practices that actually matter in 2025.
If you're building financial applications with TypeScript, check out my guide on building fintech apps with React and TypeScript.
Why TypeScript Matters More Than Ever
TypeScript adoption has exploded:
- 98% of large-scale JavaScript projects now use TypeScript
- 40% fewer bugs in production compared to plain JavaScript
- Better IDE support, refactoring, and team collaboration
Essential Configuration
Start with a strict tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}
Why strict mode? It catches bugs at compile time that would otherwise crash in production.
Type Everything Explicitly (At First)
When learning, be explicit:
// Explicit - great for learning
const users: User[] = [];
const getUser = (id: string): User | undefined => {
return users.find((user: User) => user.id === id);
};
// Later, let inference work for you
const users: User[] = [];
const getUser = (id: string) => users.find((user) => user.id === id);
Prefer Interfaces for Objects
// Prefer interface for object shapes
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// Use type for unions, intersections, and utilities
type UserRole = 'admin' | 'user' | 'guest';
type UserWithRole = User & { role: UserRole };
Why? Interfaces are more performant, extendable, and give better error messages.
Use Discriminated Unions
Model state machines with discriminated unions:
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 <Placeholder />;
case 'loading':
return <Spinner />;
case 'success':
return <DataView data={state.data} />; // TypeScript knows data exists
case 'error':
return <ErrorMessage error={state.error} />; // TypeScript knows error exists
}
}
Master Utility Types
TypeScript's built-in utility types are powerful:
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Pick specific properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Omit sensitive data
type SafeUser = Omit<User, 'password'>;
// Make all properties optional (for updates)
type UserUpdate = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
// Make all properties readonly
type ImmutableUser = Readonly<User>;
// Extract keys
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'password' | 'createdAt'
Create Custom Utility Types
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Usage: id is optional, rest required
type NewUser = PartialBy<User, 'id' | 'createdAt'>;
// Make specific properties required
type RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Deep partial (recursive)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
Use const Assertions
// Without const assertion - type is string[]
const roles = ['admin', 'user', 'guest'];
// With const assertion - type is readonly ['admin', 'user', 'guest']
const roles = ['admin', 'user', 'guest'] as const;
// Now you can derive types from it
type Role = (typeof roles)[number]; // 'admin' | 'user' | 'guest'
// Great for configs
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} as const;
Narrow Types with Type Guards
// Type predicate
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processData(data: unknown) {
if (isUser(data)) {
console.log(data.name); // TypeScript knows it's a User
}
}
// Assertion function
function assertUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Expected User');
}
}
Generic Constraints
// Basic constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// Conditional types
type ApiResponse<T> = T extends undefined
? { success: boolean }
: { success: boolean; data: T };
Avoid These Anti-Patterns
Don't Use any
// Bad - defeats the purpose of TypeScript
function processData(data: any) {
return data.foo.bar.baz; // No type safety
}
// Good - use unknown and narrow
function processData(data: unknown) {
if (isValidData(data)) {
return data.foo.bar.baz; // Type safe
}
throw new Error('Invalid data');
}
Don't Use Non-Null Assertion Carelessly
// Bad - crashes if user is undefined
const user = users.find((u) => u.id === id)!;
console.log(user.name);
// Good - handle the undefined case
const user = users.find((u) => u.id === id);
if (!user) {
throw new Error(`User ${id} not found`);
}
console.log(user.name);
Don't Over-Type
// Bad - redundant type annotation
const name: string = 'Justin'; // TypeScript infers string
const users: User[] = getUsers(); // Already returns User[]
// Good - let inference work
const name = 'Justin';
const users = getUsers();
React-Specific Patterns
// Props with children
interface CardProps {
title: string;
children: React.ReactNode;
}
// Event handlers
interface ButtonProps {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
// Generic components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
// Polymorphic components
type BoxProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, 'as' | 'children'>;
function Box<E extends React.ElementType = 'div'>({
as,
children,
...props
}: BoxProps<E>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
Zod for Runtime Validation
Combine TypeScript with Zod for runtime safety:
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Runtime validation
function createUser(data: unknown): User {
return UserSchema.parse(data); // Throws if invalid
}
// Safe parsing
function safeCreateUser(data: unknown) {
const result = UserSchema.safeParse(data);
if (result.success) {
return result.data; // Type: User
}
console.error(result.error);
return null;
}
Performance Tips
- Use
skipLibCheck: true- Faster compilation - Use project references - For monorepos
- Incremental builds -
"incremental": true - Avoid complex conditional types - They slow down the compiler
My TypeScript Stack
For projects like InsureSignal and Liquidity Hunters:
- Zod for runtime validation
- ts-pattern for exhaustive pattern matching
- type-fest for additional utility types
- tsx for running TypeScript directly
Conclusion
TypeScript's power comes from using it correctly. Start strict, use inference where it makes sense, and leverage the type system to catch bugs before they reach production.
The patterns above have saved me countless hours of debugging across multiple production applications. Master them, and you'll write safer, more maintainable code.
For more TypeScript in action, check out my projects and other blog posts.
Related Articles: