TypeScript Best Practices: Type Safety and Code Quality
TypeScript adds static type checking to JavaScript, improving code quality and developer experience. This guide covers TypeScript best practices and type safety techniques.
Last updated:Why Use TypeScript?
TypeScript is a superset of JavaScript. All JavaScript code is valid TypeScript code. It adds static type checking.
Type safety: Catches type errors at compile time. Reduces runtime errors (undefined is not a function).
Developer experience: IDE autocomplete, refactoring, go-to-definition features work with type information.
Large projects: As codebase grows, type checking makes maintenance and refactoring easier. Improves team collaboration.
Type Definition Best Practices
interface vs type: Use interface for object shapes. Use type for union/intersection types. interface can be extended with extends.
Avoid any: any disables type checking. Use unknown and perform type checking: if (typeof x === 'string') { ... }.
Strict mode: Set strict: true in tsconfig.json. This enables controls like noImplicitAny, strictNullChecks, strictFunctionTypes.
Type inference: Use TypeScript's type inference instead of explicit type annotation. const name = 'John' (inferred as string).
Literal types: Use specific values instead of string or number: type Status = 'active' | 'inactive' | 'pending'.
Advanced Type Techniques
Generic types: Create reusable, type-safe functions and classes: function identity<T>(arg: T): T { return arg; }.
Conditional types: Return different types based on type conditions: type IsString<T> = T extends string ? true : false.
Mapped types: Transform existing types: type Readonly<T> = { readonly [P in keyof T]: T[P]; }.
Utility types: Use TypeScript's built-in utility types: Partial, Required, Pick, Omit, Record, Exclude, Extract.
Template literal types: Create type-safe strings with string manipulation: type EventName = `on${Capitalize<string>}`.
TypeScript with React
Props definition: interface Props { name: string; age?: number; onClick: () => void; }. Use ? for optional props.
useState: const [count, setCount] = useState<number>(0);. Type inference is usually sufficient: useState(0) (inferred as number).
useRef: const inputRef = useRef<HTMLInputElement>(null);. Use correct type for DOM element references.
Event handlers: Use React event types like React.ChangeEvent<HTMLInputElement>, React.FormEvent<HTMLFormElement>.
Children: Use React.ReactNode (general) or React.ReactElement (only element). Add children prop with PropsWithChildren.
Error Handling and Debugging
Type assertions: Convert types with as keyword: const value = input as string. Prefer type guards when possible.
Type guards: Perform runtime type checking with typeof, instanceof, in operator, and custom type guard functions.
Non-null assertion: Specify that value is not null/undefined with ! operator: element!.value. Use only when certain.
tsconfig strictNullChecks: Handle null and undefined explicitly. Use optional chaining (?.) and nullish coalescing (??).
ESLint + TypeScript: Add extra rules with @typescript-eslint plugin. Enable rules like no-unused-vars, no-explicit-any.
Project Configuration
tsconfig.json: Set strict: true, target: 'ES2020', module: 'ESNext', moduleResolution: 'bundler', esModuleInterop: true.
Path aliases: Shorten import paths with baseUrl and paths: "@/*": ["src/*"]. import { Button } from '@/components/Button'.
Project references: Use project references for incremental build in large projects. Use references array in tsconfig.json.
Monorepo: Configure TypeScript monorepo with Turborepo or Nx. Create shared packages for types and utilities.
Declaration files: Add type definitions to JavaScript libraries with .d.ts files. Use @types/* packages.