Key Takeaways
- Avoid 'any' at all costs; use 'unknown', union types, or generics for truly unknown values
- Discriminated unions model component variants more safely than optional props with invalid combinations
- Zod provides both runtime validation and TypeScript type generation, ensuring API responses match types
- Enable strict mode and noUncheckedIndexedAccess for maximum type safety in tsconfig
- Frequent use of type assertions (as) is a code smell indicating incorrect type definitions
Typing React Components Effectively
Properly typed components serve as self-documenting contracts. Clear prop interfaces tell other developers exactly what a component expects, preventing bugs and reducing the need for documentation.
| Pattern | Use Case | Example |
|---|---|---|
| Interface for props | All components | interface ButtonProps { variant: 'primary' | 'secondary' } |
| React.FC (avoid) | Legacy codebases | Implicitly adds children, less explicit |
| Discriminated unions | Variant components | type Props = { type: 'link'; href: string } | { type: 'button'; onClick: () => void } |
| Generic components | Reusable data components | function List<T>({ items }: { items: T[] }) |
| PropsWithChildren | Wrapper components | type Props = PropsWithChildren<{ title: string }> |
The TypeScript community and React team recommend plain function declarations over React.FC. FC implicitly includes children (even when not wanted) and makes generics awkward. Use `function Button(props: ButtonProps)` instead.
- Name prop interfaces descriptively: UserCardProps, not Props or IProps
- Export prop interfaces so parent components can reference them
- Use discriminated unions when component behavior changes based on a prop variant
- Prefer interfaces over types for props (better error messages, extendable)
Advanced TypeScript Patterns for React
Advanced TypeScript patterns reduce boilerplate while maintaining strong type safety. These patterns handle complex scenarios like polymorphic components, generic hooks, and type-safe event systems.
| Utility Type | Purpose | Example Usage |
|---|---|---|
| Partial<T> | Make all properties optional | Partial<UserProfile> for update forms |
| Pick<T, K> | Select specific properties | Pick<User, 'name' | 'email'> |
| Omit<T, K> | Exclude specific properties | Omit<User, 'password'> |
| Record<K, V> | Dictionary type | Record<string, ComponentType> |
| Extract<T, U> | Extract matching union members | Extract<Status, 'active' | 'pending'> |
| ReturnType<T> | Infer function return type | ReturnType<typeof useAuth> |
If you use `as` more than once per file, your types likely do not match reality. Instead of asserting, fix the type definitions to accurately represent your data. Type assertions bypass the compiler and hide potential bugs.
Runtime Validation with Zod
TypeScript types are erased at runtime. API responses, form inputs, and external data can violate your type definitions without any runtime error. Zod bridges this gap with runtime validation that generates TypeScript types.
- Define Zod schemas for all API response shapes
- Use z.infer<typeof schema> to derive TypeScript types from Zod schemas
- Validate API responses at the boundary (fetch wrapper, API client)
- Use Zod schemas with react-hook-form for type-safe form validation
- Create shared schemas between frontend and backend for end-to-end type safety
With Zod, the schema is the single source of truth for both runtime validation and TypeScript types. This eliminates the common problem of TypeScript interfaces drifting from actual API response shapes.
TypeScript Configuration for Maximum Safety
TypeScript's default configuration is permissive. Enabling strict mode and additional checks catches significantly more bugs at compile time, reducing runtime errors in production.
| Setting | What It Does | Recommendation |
|---|---|---|
| strict: true | Enables all strict checks | Always enable |
| noUncheckedIndexedAccess | Array/object access returns T | undefined | Enable (catches null errors) |
| exactOptionalPropertyTypes | Distinguishes undefined from missing | Enable for new projects |
| noImplicitReturns | All code paths must return | Enable |
| forceConsistentCasingInFileNames | Prevents case-sensitivity bugs | Always enable |
- Start new projects with strict: true from day one
- For existing projects, enable strict checks incrementally to avoid overwhelming changes
- noUncheckedIndexedAccess is the single most impactful non-default setting
- Use // @ts-expect-error (not @ts-ignore) for intentional suppressions with error messages
Frequently Asked Questions
Related services from Verbsz Technologies
If this article is relevant to a project on your roadmap, these are the services our team typically delivers.
