TypeScript has evolved from "nice to have" to "non-negotiable" for serious frontend projects. But most teams barely scratch the surface β using `any` when stuck, ignoring strict mode, and missing the advanced features that prevent entire categories of bugs. This guide covers the TypeScript patterns that separate senior engineers from juniors in 2026.
The Non-Negotiable Baseline
Strict Mode Is Not Optional
// tsconfig.json β Minimum viable config
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",// THESE ARE NON-NEGOTIABLE "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true,
// Modern output "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true,
// Path aliases "baseUrl": ".", "paths": { "@/*": ["src/*"], "@/components/*": ["src/components/*"], "@/lib/*": ["src/lib/*"], "@/hooks/*": ["src/hooks/*"] } }, "include": ["src/**/*", "*.config.ts"], "exclude": ["node_modules", "dist", "build"] }
Why These Flags Matter
| Flag | What It Catches | Real-World Impact | |------|-----------------|-------------------| | `strict` | Implicit `any`, missing types | Eliminates 80% of runtime errors | | `noUncheckedIndexedAccess` | `arr[0]` possibly `undefined` | Eliminates "cannot read property of undefined" | | `exactOptionalPropertyTypes` | `{ foo?: string }` vs `{ foo: string \| undefined }` | Distinguishes missing vs explicit undefined | | `noImplicitReturns` | Missing return in code paths | Catches missing returns in complex functions | | `noUnusedLocals/Parameters` | Dead code | Keeps codebase clean, catches typos |
Advanced Type Patterns
1. Discriminated Unions (The Secret Weapon)
// β BAD β Optional properties, unclear state
interface UserState {
user?: User;
error?: string;
loading?: boolean;
}// β GOOD β Exhaustive, type-safe states type UserState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; user: User } | { status: 'error'; error: Error };
function renderUserState(state: UserState) { switch (state.status) { case 'idle': return
2. Template Literal Types for API Safety
// HTTP Methods
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';// API Endpoints with params type ApiEndpoint = | `/users/${string}` | `/posts/${string}` | `/users/${string}/posts/${string}`;
// Type-safe fetch wrapper async function apiRequest
if (!res.ok) throw new Error(`API Error: ${res.status}`); return res.json(); }
// Usage β fully typed const user = await apiRequest
3. Branded Types for Domain Safety
// Prevents mixing up UserId vs PostId
type Brand<T, B> = T & { __brand: B };type UserId = Brand
// Constructors const createUserId = (id: string): UserId => id as UserId; const createEmail = (email: string): Email => { if (!email.includes('@')) throw new Error('Invalid email'); return email as Email; };
// Usage β compiler prevents mixing function getUser(id: UserId) { /* ... */ } function getPost(id: PostId) { /* ... */ }
const userId = createUserId('123'); const postId = createPostId('456');
getUser(userId); // β getUser(postId); // β Type '"PostId"' not assignable to '"UserId"'
4. Conditional Types for API Responses
// Extract response type from API function
type ApiResponse<T> = T extends Promise<{ data: infer D }> ? D : never;// Usage async function fetchUser(id: string) { const res = await fetch(`/api/users/${id}`); return res.json(); // { data: User } }
type User = ApiResponse
5. Mapped Types for Form Handling
// Transform any type into form field config
type FormFields<T> = {
[K in keyof T]: {
label: string;
type: T[K] extends string ? 'text' :
T[K] extends number ? 'number' :
T[K] extends boolean ? 'checkbox' : 'text';
required: true;
placeholder?: string;
};
};interface UserForm { name: string; email: string; age: number; isActive: boolean; }
// Result type: /* { name: { label: string; type: 'text'; required: true; placeholder?: string }; email: { label: string; type: 'text'; required: true; placeholder?: string }; age: { label: string; type: 'number'; required: true; placeholder?: string }; isActive: { label: string; type: 'checkbox'; required: true; placeholder?: string }; } */
// Auto-generate form fields function generateFields
Utility Types Mastery
Essential Built-in Utilities
// Partial β All properties optional
type PartialUser = Partial<User>;
// { name?: string; email?: string; age?: number }// Required β All properties required type RequiredUser = Required
// Pick β Select subset type UserPreview = Pick
// Omit β Exclude properties type UserWithoutPassword = Omit
// Record β Object with specific keys type UserRoles = Record
// Exclude/Extract β Union manipulation type NonAdminRoles = Exclude
// Parameters/ReturnType β Function introspection type FetchUserParams = Parameters
type FetchUserReturn = ReturnType
// ConstructorParameters/InstanceType β Class introspection class UserService { constructor(private api: ApiClient) {} async getUser(id: string) { /* ... */ } }
type ServiceDeps = ConstructorParameters
type ServiceInstance = InstanceType
Advanced: Infer in Conditional Types
// Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StringArray = string[];
type Element = ArrayElement<StringArray>; // string// Extract Promise resolution type Awaited
// Extract function return type type ReturnType
// Extract component props type ComponentProps
Type-Safe API Layer
End-to-End Type Safety with tRPC
// server/routers/user.ts
import { z } from 'zod';
import { createTRPCRouter, publicProcedure } from '../trpc';export const userRouter = createTRPCRouter({ getById: publicProcedure .input(z.object({ id: z.string().uuid() })) .query(async ({ ctx, input }) => { return ctx.db.user.findUnique({ where: { id: input.id } }); }),
create: publicProcedure .input(z.object({ name: z.string().min(2).max(100), email: z.string().email(), age: z.number().min(13).max(120).optional(), })) .mutation(async ({ ctx, input }) => { return ctx.db.user.create({ data: input }); }),
list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(20), cursor: z.string().optional(), })) .query(async ({ ctx, input }) => { const users = await ctx.db.user.findMany({ take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: 'desc' }, });
let nextCursor: string | undefined; if (users.length > input.limit) { const next = users.pop(); nextCursor = next!.id; }
return { users, nextCursor }; }), });
// Frontend β Fully typed, no manual types needed
import { trpc } from '@/lib/trpc';function UserList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = trpc.user.list.useInfiniteQuery({ limit: 20, getNextPageParam: (lastPage) => lastPage.nextCursor, });
// data is fully typed: { pages: User[][]; pageParams: string[] } return (
Zod + TypeScript = Runtime + Compile-time Safety
// schemas/user.ts
import { z } from 'zod';export const UserSchema = z.object({ id: z.string().uuid(), name: z.string().min(2).max(100), email: z.string().email(), age: z.number().int().min(13).max(120).optional(), role: z.enum(['user', 'admin', 'moderator']).default('user'), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), });
export type User = z.infer
// Form validation with React Hook Form import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod';
function UserForm() { const form = useForm
// form.handleSubmit, form.register β fully typed! return
; }React Patterns with TypeScript
Component Props with Discriminated Unions
// Variants as discriminated union
type ButtonProps =
| { variant: 'primary'; onClick: () => void; children: React.ReactNode }
| { variant: 'secondary'; onClick: () => void; children: React.ReactNode }
| { variant: 'link'; href: string; children: React.ReactNode }
| { variant: 'destructive'; onClick: () => void; children: React.ReactNode; confirm?: string };export function Button(props: ButtonProps) { switch (props.variant) { case 'primary': return ; case 'secondary': return ; case 'link': return {props.children}; case 'destructive': return ( ); } }
Generic Components with Constraints
// Type-safe select with generics
interface SelectOption<T> {
value: T;
label: string;
}interface SelectProps
function Select
// Usage β fully typed const statuses = ['active', 'inactive', 'pending'] as const; type Status = typeof statuses[number];
Context with Proper Typing
// context/AuthContext.tsx
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
isLoading: boolean;
}const AuthContext = createContext
export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState
const login = async (email: string, password: string) => { const user = await authApi.login(email, password); setUser(user); };
return (
// Typed hook β no null checks needed export function useAuth() { const context = useContext(AuthContext); if (!context) throw new Error('useAuth must be used within AuthProvider'); return context; }
Type-Safe Forms with React Hook Form + Zod
// schemas/contact.ts
import { z } from 'zod';export const ContactSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters').max(100), email: z.string().email('Invalid email address'), subject: z.enum(['support', 'sales', 'partnership', 'other']), message: z.string().min(10, 'Message too short').max(5000), newsletter: z.boolean().default(false), });
export type ContactForm = z.infer
// Component import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod';
export function ContactForm() { const form = useForm
const onSubmit = async (data: ContactForm) => { await api.post('/contact', data); toast.success('Message sent!'); };
return (
); }Migration Strategies
From JavaScript to TypeScript
# 1. Add TypeScript
npm install -D typescript @types/react @types/node# 2. Create tsconfig.json (use strict config above)
# 3. Rename files incrementally # .js β .tsx (components), .ts (utils)
# 4. Add types incrementally # Start with: interfaces for props, function signatures # Then: strict null checks, generics, advanced patterns
# 5. Enable strict mode gradually # "strict": false β true (after fixing errors)
Common Migration Patterns
// Before (JS)
function Button({ children, onClick, variant = 'primary' }) {
return <button className={`btn btn-${variant}`} onClick={onClick}>
{children}
</button>;
}// After (TS) β Incremental interface ButtonProps { children: React.ReactNode; onClick?: () => void; variant?: 'primary' | 'secondary'; }
function Button({ children, onClick, variant = 'primary' }: ButtonProps) { return ; }
The UI Designer TypeScript Standard
Every project we build enforces:
// tsconfig.json β Our standard
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": true
}
}**Our rules:**
- **Zero `any`** β Use `unknown` + type guards instead
- **Zero `@ts-ignore`** β Fix the type, don't suppress
- **Discriminated unions over optional props** β State machines, not maybe-props
- **Generics for reusable components** β Select, Table, Modal, Form
- **Zod for runtime validation** β Schema = source of truth
- **tRPC for APIs** β End-to-end type safety
- **Zero `type: any` in tests** β Test types too
The 2026 TypeScript Checklist
- [ ] `strict: true` in all projects
- [ ] Zero `any` in production code
- [ ] Zero `@ts-ignore` / `@ts-expect-error`
- [ ] Discriminated unions for all state machines
- [ ] Zod schemas for all external data (API, forms, env)
- [ ] tRPC or typed fetch for all API calls
- [ ] Generics for all reusable components
- [ ] Branded types for domain IDs (UserId, PostId, etc.)
- [ ] Template literal types for API endpoints
- [ ] `noUnusedLocals/Parameters` enabled
- [ ] Type coverage β₯95% (check with `type-coverage`)
- [ ] Mutation testing on critical types (Stryker)
Ready to Level Up Your TypeScript?
**Book a TypeScript architecture audit** β we'll audit your codebase, identify type safety gaps, set up strict config, and implement advanced patterns your team will actually use.
ui-designer.in/typescript-audit

