Advanced TypeScript Patterns for React
Master advanced TypeScript patterns that make your React code type-safe, maintainable, and scalable. Learn generic components, utility types, discriminated unions, type inference, and advanced patterns used by senior engineers.
🧬 1. Generic Components
Generics are like the Swiss Army knife of TypeScript! 🔧 Build components that adapt to ANY data type while keeping full type safety. Write once, use everywhere — your future self will thank you! 🎯
🔴 Impact: CRITICAL — Generic components eliminate code duplication and catch bugs at compile time — this is TypeScript's superpower! 💥
📋 In this section: Generic List Components • Generic Form Builders • Type Constraints • Multi-type Generics
// ✅ Generic List Componentinterface ListProps<T> {items: T[];renderItem: (item: T) => React.ReactNode;keyExtractor: (item: T) => string | number;emptyMessage?: string;}function List<T>({items,renderItem,keyExtractor,emptyMessage = "No items"}: ListProps<T>) {if (items.length === 0) {return <div>{emptyMessage}</div>;}return (<ul>{items.map(item => (<li key={keyExtractor(item)}>{renderItem(item)}</li>))}</ul>);}// Usage with different typesinterface User {id: string;name: string;email: string;}interface Product {id: number;title: string;price: number;}function UserList({ users }: { users: User[] }) {return (<Listitems={users}keyExtractor={user => user.id}renderItem={user => (<div><h3>{user.name}</h3><p>{user.email}</p></div>)}/>);}function ProductList({ products }: { products: Product[] }) {return (<Listitems={products}keyExtractor={product => product.id}renderItem={product => (<div><h3>{product.title}</h3><p>${product.price}</p></div>)}/>);}// ✅ Generic Form Componentinterface FormField<T> {name: keyof T;label: string;type?: 'text' | 'email' | 'number';required?: boolean;}interface FormProps<T extends Record<string, any>> {fields: FormField<T>[];initialValues: T;onSubmit: (values: T) => void;}function Form<T extends Record<string, any>>({fields,initialValues,onSubmit}: FormProps<T>) {const [values, setValues] = useState<T>(initialValues);const handleSubmit = (e: React.FormEvent) => {e.preventDefault();onSubmit(values);};return (<form onSubmit={handleSubmit}>{fields.map(field => (<div key={String(field.name)}><label>{field.label}{field.required && <span>*</span>}</label><inputtype={field.type || 'text'}value={String(values[field.name] || '')}onChange={e => setValues({...values,[field.name]: e.target.value})}required={field.required}/></div>))}<button type="submit">Submit</button></form>);}
🛠️ 2. Utility Types & Mapped Types
TypeScript ships with an incredible toolbox of utility types — and you can build your own! 🧰 From Partial to Pick, Omit to Record, these type transformations are like magic spells for your codebase. Abracadabra, your types are perfect! ✨
🟠 Impact: HIGH — Utility types turn complex type manipulation into one-liners — they're the cheat codes of TypeScript! 🎮
📋 In this section: Built-in Utility Types • Custom Mapped Types • Conditional Types • Template Literal Types • Branded Types
// ✅ Built-in Utility Typesinterface User {id: string;name: string;email: string;age: number;isActive: boolean;}// Partial - all properties optionaltype PartialUser = Partial<User>;// { id?: string; name?: string; ... }// Required - all properties requiredtype RequiredUser = Required<PartialUser>;// Pick - select specific propertiestype UserPreview = Pick<User, 'id' | 'name'>;// { id: string; name: string; }// Omit - exclude specific propertiestype UserWithoutId = Omit<User, 'id'>;// { name: string; email: string; age: number; isActive: boolean; }// Record - create object typetype UserRoles = Record<string, 'admin' | 'user' | 'guest'>;// { [key: string]: 'admin' | 'user' | 'guest' }// ✅ Custom Utility Types// Make all properties nullabletype Nullable<T> = {[P in keyof T]: T[P] | null;};// Make all properties readonlytype ReadonlyDeep<T> = {readonly [P in keyof T]: T[P] extends object ? ReadonlyDeep<T[P]> : T[P];};// Extract function return typetype ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;// Extract function parameterstype Parameters<T> = T extends (...args: infer P) => any ? P : never;// ✅ Mapped Types for Component Propstype ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;// Usagefunction MyComponent({ name, age }: { name: string; age: number }) {return <div>{name} is {age}</div>;}type MyComponentProps = ComponentProps<typeof MyComponent>;// { name: string; age: number; }// ✅ Conditional Typestype NonNullable<T> = T extends null | undefined ? never : T;type ArrayElement<T> = T extends (infer U)[] ? U : never;type PromiseType<T> = T extends Promise<infer U> ? U : never;// ✅ Template Literal Typestype EventName<T extends string> = `on${Capitalize<T>}`;type ClickEvent = EventName<'click'>; // 'onClick'type ChangeEvent = EventName<'change'>; // 'onChange'// ✅ Branded Types (Nominal Typing)type UserId = string & { readonly brand: unique symbol };type ProductId = string & { readonly brand: unique symbol };function createUserId(id: string): UserId {return id as UserId;}function createProductId(id: string): ProductId {return id as ProductId;}// TypeScript prevents mixing themconst userId = createUserId('123');const productId = createProductId('456');// userId === productId // Type error!
🏷️ 3. Discriminated Unions
Discriminated unions are the secret sauce of bulletproof TypeScript! 🔒 They let TypeScript automatically narrow types based on a tag field — no more runtime surprises. It's like giving your compiler X-ray vision! 👁️
🔴 Impact: CRITICAL — Discriminated unions eliminate entire categories of bugs and make impossible states truly impossible! 🛡️
📋 In this section: API Response Patterns • Component Variants • State Machines • Exhaustive Checks
// ✅ API Response Patterntype ApiResponse<T> =| { status: 'loading' }| { status: 'success'; data: T }| { status: 'error'; error: string };function useApi<T>(url: string): ApiResponse<T> {const [response, setResponse] = useState<ApiResponse<T>>({ status: 'loading' });useEffect(() => {fetch(url).then(res => res.json()).then(data => setResponse({ status: 'success', data })).catch(error => setResponse({ status: 'error', error: error.message }));}, [url]);return response;}// Usage - TypeScript knows the shape based on statusfunction UserProfile({ userId }: { userId: string }) {const response = useApi<User>(`/api/users/${userId}`);if (response.status === 'loading') {return <Spinner />;}if (response.status === 'error') {return <Error message={response.error} />;}// TypeScript knows response.data exists herereturn <div>{response.data.name}</div>;}// ✅ Component Variantstype ButtonVariant = 'primary' | 'secondary' | 'danger';type ButtonProps =| { variant: 'primary'; icon?: React.ReactNode }| { variant: 'secondary'; outline?: boolean }| { variant: 'danger'; confirm?: boolean };function Button(props: ButtonProps & { children: React.ReactNode }) {switch (props.variant) {case 'primary':return (<button className="btn-primary">{props.icon}{props.children}</button>);case 'secondary':return (<button className={props.outline ? 'btn-outline' : 'btn-secondary'}>{props.children}</button>);case 'danger':return (<buttonclassName="btn-danger"onClick={props.confirm ? handleConfirm : undefined}>{props.children}</button>);}}// ✅ State Machine Patterntype AsyncState<T> =| { type: 'idle' }| { type: 'loading' }| { type: 'success'; data: T }| { type: 'error'; error: Error };function useAsyncState<T>() {const [state, setState] = useState<AsyncState<T>>({ type: 'idle' });const execute = useCallback(async (fn: () => Promise<T>) => {setState({ type: 'loading' });try {const data = await fn();setState({ type: 'success', data });} catch (error) {setState({ type: 'error', error: error as Error });}}, []);return { state, execute };}
🪝 4. Advanced Hooks Typing
Custom hooks are awesome, but typing them correctly? That's where the real magic happens! 🎩 Learn to type overloads, factory patterns, and complex return types so your hooks are as type-safe as they are powerful. Level up your hook game! 📈
🟠 Impact: HIGH — Properly typed hooks make your entire codebase safer — every consumer gets perfect autocomplete and error checking! 🎯
📋 In this section: Typed Custom Hooks • Hook Overloads • Hook Factories • Generic Hook Patterns
// ✅ Typed Custom Hookfunction useLocalStorage<T>(key: string,initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {const [storedValue, setStoredValue] = useState<T>(() => {try {const item = window.localStorage.getItem(key);return item ? JSON.parse(item) : initialValue;} catch {return initialValue;}});const setValue = useCallback((value: T | ((val: T) => T)) => {try {const valueToStore = value instanceof Function ? value(storedValue) : value;setStoredValue(valueToStore);window.localStorage.setItem(key, JSON.stringify(valueToStore));} catch (error) {console.error(error);}}, [key, storedValue]);return [storedValue, setValue];}// ✅ Hook with Overloadsfunction useFetch<T>(url: string): {data: T | null;loading: boolean;error: Error | null;};function useFetch<T>(url: string, options: { immediate: false }): {data: T | null;loading: boolean;error: Error | null;execute: () => Promise<void>;};function useFetch<T>(url: string,options?: { immediate?: boolean }): any {const [data, setData] = useState<T | null>(null);const [loading, setLoading] = useState(false);const [error, setError] = useState<Error | null>(null);const execute = useCallback(async () => {setLoading(true);try {const response = await fetch(url);const result = await response.json();setData(result);} catch (err) {setError(err as Error);} finally {setLoading(false);}}, [url]);useEffect(() => {if (options?.immediate !== false) {execute();}}, [execute, options?.immediate]);return options?.immediate === false? { data, loading, error, execute }: { data, loading, error };}// ✅ Type-safe Hook Factoryfunction createUseApi<TData, TError = Error>(endpoint: string) {return function useApi() {const [data, setData] = useState<TData | null>(null);const [error, setError] = useState<TError | null>(null);const [loading, setLoading] = useState(false);const fetchData = useCallback(async () => {setLoading(true);try {const response = await fetch(endpoint);const result = await response.json();setData(result);} catch (err) {setError(err as TError);} finally {setLoading(false);}}, [endpoint]);useEffect(() => {fetchData();}, [fetchData]);return { data, error, loading, refetch: fetchData };};}// Usageinterface User {id: string;name: string;}const useUser = createUseApi<User>('/api/user');// useUser returns { data: User | null, error: Error | null, ... }
🛡️ 5. Type Guards & Assertions
Type guards are your runtime bodyguards! 💂 They help TypeScript narrow types safely at runtime, while assertions let you tell the compiler "trust me, I know what this is." Together, they're the dynamic duo of type safety! 🦸♂️🦸♀️
🔵 Impact: MEDIUM — Type guards bridge the gap between compile-time and runtime safety — essential for working with unknown data! 🌉
📋 In this section: Custom Type Guards • Discriminated Union Guards • Assertion Functions • Error Type Guards
// ✅ Type Guardsfunction isString(value: unknown): value is string {return typeof value === 'string';}function isUser(value: unknown): value is User {return (typeof value === 'object' &&value !== null &&'id' in value &&'name' in value &&typeof (value as any).id === 'string' &&typeof (value as any).name === 'string');}// Usagefunction processValue(value: unknown) {if (isString(value)) {// TypeScript knows value is string hereconsole.log(value.toUpperCase());}if (isUser(value)) {// TypeScript knows value is User hereconsole.log(value.name);}}// ✅ Discriminated Union Type Guardtype Result<T> =| { success: true; data: T }| { success: false; error: string };function isSuccess<T>(result: Result<T>): result is { success: true; data: T } {return result.success === true;}function handleResult<T>(result: Result<T>) {if (isSuccess(result)) {// TypeScript knows result.data existsconsole.log(result.data);} else {// TypeScript knows result.error existsconsole.error(result.error);}}// ✅ Assertion Functionsfunction assertIsString(value: unknown): asserts value is string {if (typeof value !== 'string') {throw new Error('Expected string');}}function assertIsUser(value: unknown): asserts value is User {if (!isUser(value)) {throw new Error('Expected User');}}// Usagefunction processUser(data: unknown) {assertIsUser(data);// TypeScript knows data is User after assertionconsole.log(data.name);}// ✅ Custom Error with Type Guardclass ValidationError extends Error {constructor(public field: string,message: string) {super(message);this.name = 'ValidationError';}}function isValidationError(error: unknown): error is ValidationError {return error instanceof ValidationError;}function handleError(error: unknown) {if (isValidationError(error)) {console.error(`Validation failed for \${error.field}: \${error.message}`);} else {console.error('Unknown error:', error);}}