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);}}