typescript-best-practices
TypeScript Best Practices
Type Safety
- Enable
strict: trueintsconfig.json— never disable strict checks in production code. - Prefer
unknownoverany. Ifanyis unavoidable, add a comment explaining why and narrow it immediately. - Use
satisfiesto validate a value matches a type while preserving its narrowed literal type:
const config = {
endpoint: "/api/users",
timeout: 3000,
} satisfies Config;
- Prefer type narrowing (type guards,
inoperator,instanceof) over type assertions (as).
Type Inference
- Let TypeScript infer when the type is obvious — don't annotate what the compiler already knows:
// Redundant
const name: string = "Graham";
// Let it infer
const name = "Graham";
- Always annotate function return types for exported/public functions — it catches accidental return type changes and improves IDE performance:
export function getUser(id: string): User | undefined {
return users.get(id);
}
- Annotate function parameters — they cannot be inferred from implementation.
Types vs Interfaces
- Use
typefor unions, intersections, mapped types, and utility types. - Use
interfacefor object shapes that may be extended or implemented. - Be consistent within a codebase — pick one default and stick with it.
Enums and Constants
- Prefer
as constobjects overenum:
const Status = {
Active: "active",
Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];
- This gives you type safety, tree-shaking, and no runtime enum overhead.
Null Handling
- Prefer explicit
| undefinedin types over optional properties when the distinction matters. - Use optional chaining (
?.) and nullish coalescing (??) over manual null checks. - Avoid non-null assertions (
!) — narrow the type instead.
Generics
- Name generic parameters descriptively when there are multiple:
TInput,TOutputinstead ofT,U. - Constrain generics with
extendsto communicate intent:
function merge<T extends Record<string, unknown>>(a: T, b: Partial<T>): T {
return { ...a, ...b };
}
- Avoid over-genericizing — if a function only ever handles one type, don't make it generic.
Utility Types
- Use built-in utility types (
Partial,Required,Pick,Omit,Record,Readonly) instead of reimplementing them. Readonly<T>for data that should not be mutated.PickandOmitto derive subsets from existing types rather than duplicating fields.
Error Handling
- Type errors explicitly — don't rely on
catch (e)beingany:
try {
await fetchData();
} catch (error) {
if (error instanceof ApiError) {
handleApiError(error);
}
throw error;
}
- Create typed error classes for domain-specific errors.
Module Organization
- One type/interface per concern — avoid monolithic
types.tsfiles. - Co-locate types with the code that uses them.
- Export types from barrel files only when they form part of the public API.
- Use
import type/export typefor type-only imports to enable proper tree-shaking.
Naming Conventions
- PascalCase for types, interfaces, enums, and classes.
- camelCase for variables, functions, and methods.
- UPPER_SNAKE_CASE for true constants (compile-time values).
- Don't prefix interfaces with
Ior types withT— it's not C#.
More from grahamcrackers/skills
bulletproof-react-patterns
Bulletproof React architecture patterns for scalable, maintainable applications. Covers feature-based project structure, component patterns, state management boundaries, API layer design, error handling, security, and testing strategies. Use when structuring a React project, designing application architecture, organizing features, or when the user asks about React project structure or scalable patterns.
44react-aria-components
React Aria Components patterns for building accessible, unstyled UI with composition-based architecture. Covers component structure, styling with Tailwind and CSS, render props, collections, forms, selections, overlays, and drag-and-drop. Use when building accessible components, using react-aria-components, creating design systems, or when the user asks about React Aria, accessible UI primitives, or headless component libraries.
15clean-code-principles
Clean code principles for readable, maintainable TypeScript and React codebases. Covers naming, functions, abstraction, composition, error handling, comments, and code smells. Use when writing new code, refactoring, reviewing code quality, or when the user asks about clean code, readability, or maintainability.
10tanstack-query
TanStack Query v5 patterns for server state management, caching, mutations, optimistic updates, and query organization. Use when working with TanStack Query, React Query, server state, data fetching hooks, or when the user asks about caching strategies, query invalidation, or mutation patterns.
8zustand
Zustand state management patterns for React including store design, selectors, slices, middleware (immer, persist, devtools), and async actions. Use when managing client-side state, creating stores, working with Zustand, or when the user asks about global state management, store patterns, or state persistence.
7react-best-practices
Modern React 19 patterns for components, hooks, state management, performance, and project structure. Use when writing React components, reviewing React code, designing component APIs, or when the user asks about React conventions, architecture, or best practices.
7