Advanced TypeScript Patterns for React Applications
TypeScript transforms React development by catching errors before runtime and providing excellent IDE support. Let's explore advanced patterns that take your type safety to the next level.
Generic Components
Create reusable, type-safe components with generics.
Basic Generic Component
class="keyword">interface ListProps {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
class="keyword">class="keyword">function List({ items, renderItem, keyExtractor }: ListProps) {
class="keyword">class="keyword">return (
{items.map((item) => (
keyExtractor(item)}>{renderItem(item)}
))}
);
}
class="keyword">class="comment">// Usage with class="keyword">type inference
class="keyword">interface User {
id: number;
name: string;
email: string;
}
class="keyword">class="keyword">function UserList({ users }: { users: User[] }) {
class="keyword">class="keyword">return (
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => (
{user.name}
{user.email}
)}
/>
);
}
Constrained Generics
class="keyword">interface HasId {
id: string | number;
}
class="keyword">interface TablePropsclass="keyword">extends HasId> {
data: T[];
columns: Array<{
key: keyof T;
header: string;
render?: (value: T[keyof T], item: T) => React.ReactNode;
}>;
onRowClick?: (item: T) => class="keyword">void;
}
class="keyword">class="keyword">function Tableclass="keyword">extends HasId>({ data, columns, onRowClick }: TableProps) {
class="keyword">class="keyword">return (
{columns.map((col) => (
String(col.key)}>{col.header}
))}
{data.map((item) => (
key={item.id}
onClick={() => onRowClick?.(item)}
className=class="keyword">class="string">"cursor-pointer hover:bg-gray-100"
>
{columns.map((col) => (
String(col.key)}>
{col.render
? col.render(item[col.key], item)
: String(item[col.key])}
))}
))}
);
}
class="keyword">class="comment">// Usage
class="keyword">interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
class="keyword">class="keyword">function ProductTable({ products }: { products: Product[] }) {
class="keyword">class="keyword">return (
data={products}
columns={[
{ key: class="keyword">class="string">"name", header: class="keyword">class="string">"Product Name" },
{
key: class="keyword">class="string">"price",
header: class="keyword">class="string">"Price",
render: (value) => class="keyword">class="string">$${value.toFixed(2)},
},
{
key: class="keyword">class="string">"inStock",
header: class="keyword">class="string">"Status",
render: (value) => (value ? class="keyword">class="string">"✅ In Stock" : class="keyword">class="string">"❌ Out of Stock"),
},
]}
onRowClick={(product) => console.log(product)}
/>
);
}
Discriminated Unions
Type-safe state management with discriminated unions.
API State Pattern
app.ts
class="keyword">type ApiState =
| { status: class="keyword">class="string">"idle" }
| { status: class="keyword">class="string">"loading" }
| { status: class="keyword">class="string">"success"; data: T }
| { status: class="keyword">class="string">"error"; error: Error };
class="keyword">class="keyword">function useApi(fetcher: () => Promise) {
class="keyword">class="keyword">const [state, setState] = useState>({ status: class="keyword">class="string">"idle" });
class="keyword">class="keyword">const execute = class="keyword">class="keyword">async () => {
setState({ status: class="keyword">class="string">"loading" });
class="keyword">try {
class="keyword">class="keyword">const data = class="keyword">class="keyword">await fetcher();
setState({ status: class="keyword">class="string">"success", data });
} class="keyword">catch (error) {
setState({
status: class="keyword">class="string">"error",
error: error class="keyword">instanceof Error ? error : class="keyword">new Error(class="keyword">class="string">"Unknown error"),
});
}
};
class="keyword">class="keyword">return { state, execute };
}
class="keyword">class="comment">// Usage with class="keyword">type narrowing
class="keyword">class="keyword">function UserProfile({ userId }: { userId: string }) {
class="keyword">class="keyword">const { state, execute } = useApi(() => fetchUser(userId));
useEffect(() => {
execute();
}, [userId]);
class="keyword">class="comment">// TypeScript knows the exact shape based on status
class="keyword">switch (state.status) {
class="keyword">case class="keyword">class="string">"idle":
class="keyword">class="keyword">return ;
class="keyword">case class="keyword">class="string">"loading":
class="keyword">class="keyword">return ;
class="keyword">case class="keyword">class="string">"success":
class="keyword">class="comment">// TypeScript knows state.data exists here
class="keyword">class="keyword">return (
{state.data.name}
{state.data.email}
);
class="keyword">case class="keyword">class="string">"error":
class="keyword">class="comment">// TypeScript knows state.error exists here
class="keyword">class="keyword">return ;
}
}
Form State Pattern
app.ts
class="keyword">type FormField = {
value: T;
error?: string;
touched: boolean;
};
class="keyword">type FormState = {
[K in keyof T]: FormField;
};
class="keyword">class="keyword">function useFormclass="keyword">extends Record>(
initialValues: T,
validate: (values: T) => Partial>
) {
class="keyword">class="keyword">const [state, setState] = useState>(() => {
class="keyword">class="keyword">const initial = {} as FormState;
class="keyword">class="keyword">for (class="keyword">class="keyword">const key in initialValues) {
initial[key] = {
value: initialValues[key],
touched: false,
};
}
class="keyword">class="keyword">return initial;
});
class="keyword">class="keyword">const setValue = class="keyword">extends keyof T>(field: K, value: T[K]) => {
setState((prev) => ({
...prev,
[field]: {
...prev[field],
value,
touched: true,
},
}));
};
class="keyword">class="keyword">const validateForm = () => {
class="keyword">class="keyword">const values = Object.keys(state).reduce((acc, key) => {
acc[key as keyof T] = state[key as keyof T].value;
class="keyword">class="keyword">return acc;
}, {} as T);
class="keyword">class="keyword">const errors = validate(values);
setState((prev) => {
class="keyword">class="keyword">const next = { ...prev };
class="keyword">class="keyword">for (class="keyword">class="keyword">const key in next) {
next[key] = {
...next[key],
error: errors[key],
};
}
class="keyword">class="keyword">return next;
});
class="keyword">class="keyword">return Object.keys(errors).length === 0;
};
class="keyword">class="keyword">return { state, setValue, validateForm };
}
class="keyword">class="comment">// Usage
class="keyword">interface LoginForm {
email: string;
password: string;
}
class="keyword">class="keyword">function LoginPage() {
class="keyword">class="keyword">const { state, setValue, validateForm } = useForm(
{ email: class="keyword">class="string">"", password: class="keyword">class="string">"" },
(values) => {
class="keyword">class="keyword">const errors: Partial> = {};
class="keyword">class="keyword">if (!values.email) {
errors.email = class="keyword">class="string">"Email is required";
} class="keyword">class="keyword">else class="keyword">class="keyword">if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = class="keyword">class="string">"Email is invalid";
}
class="keyword">class="keyword">if (!values.password) {
errors.password = class="keyword">class="string">"Password is required";
} class="keyword">class="keyword">else class="keyword">class="keyword">if (values.password.length < 8) {
errors.password = class="keyword">class="string">"Password must be at least 8 characters";
}
class="keyword">class="keyword">return errors;
}
);
class="keyword">class="keyword">const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
class="keyword">class="keyword">if (validateForm()) {
class="keyword">class="comment">// Submit form
}
};
class="keyword">class="keyword">return (
);
}
Conditional Types
Create types that change based on conditions.
Component Props Based on Variant
app.ts
class="keyword">type ButtonPropsclass="keyword">extends class="keyword">class="string">'link' | class="keyword">class="string">'button'> = {
variant: V
children: React.ReactNode
className?: string
} & (V class="keyword">extends class="keyword">class="string">'link'
? { href: string; target?: string }
: { onClick: () => class="keyword">void; class="keyword">type?: class="keyword">class="string">'button' | class="keyword">class="string">'submit' | class="keyword">class="string">'reset' }
)
class="keyword">class="keyword">function Buttonclass="keyword">extends class="keyword">class="string">'link' | class="keyword">class="string">'button'>(props: ButtonProps) {
class="keyword">class="keyword">const { variant, children, className } = props
class="keyword">class="keyword">if (variant === class="keyword">class="string">'link') {
class="keyword">class="keyword">const { href, target } = props as ButtonProps<class="keyword">class="string">'link'>
class="keyword">class="keyword">return (
{children}
)
}
class="keyword">class="keyword">const { onClick, class="keyword">type } = props as ButtonProps<class="keyword">class="string">'button'>
class="keyword">class="keyword">return (
)
}
class="keyword">class="comment">// Usage - TypeScript enforces correct props
class="keyword">class="comment">// ❌ TypeScript error: href not allowed class="keyword">class="keyword">for button variant
Extract Type from Array
app.ts
class="keyword">type ArrayElement = T class="keyword">extends (infer U)[] ? U : never;
class="keyword">class="keyword">const users = [
{ id: 1, name: class="keyword">class="string">"Alice", role: class="keyword">class="string">"admin" },
{ id: 2, name: class="keyword">class="string">"Bob", role: class="keyword">class="string">"user" },
] as class="keyword">class="keyword">const;
class="keyword">type User = ArrayElement<class="keyword">typeof users>;
class="keyword">class="comment">// Type: { id: 1 | 2, name: class="keyword">class="string">'Alice' | class="keyword">class="string">'Bob', role: class="keyword">class="string">'admin' | class="keyword">class="string">'user' }
Utility Types for React
Powerful utility types for common patterns.
Props Extraction
app.ts
class="keyword">class="comment">// Extract props class="keyword">from existing component
class="keyword">type ButtonProps = React.ComponentProps<class="keyword">class="string">"button">;
class="keyword">type InputProps = React.ComponentProps<class="keyword">class="string">"input">;
class="keyword">class="comment">// Extend with custom props
class="keyword">interface CustomButtonProps class="keyword">extends ButtonProps {
isLoading?: boolean;
variant?: class="keyword">class="string">"primary" | class="keyword">class="string">"secondary";
}
class="keyword">class="keyword">function CustomButton({
isLoading,
variant = class="keyword">class="string">"primary",
children,
disabled,
...props
}: CustomButtonProps) {
class="keyword">class="keyword">return (
);
}
Omit and Pick
app.ts
class="keyword">interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
class="keyword">class="comment">// Public user(without password)
class="keyword">type PublicUser = Omitclass="keyword">class="string">"password">;
class="keyword">class="comment">// Only credentials
class="keyword">type UserCredentials = Pickclass="keyword">class="string">"email" | class="keyword">class="string">"password">;
class="keyword">class="comment">// Partial class="keyword">class="keyword">for updates
class="keyword">type UserUpdate = Partialclass="keyword">class="string">"id" | class="keyword">class="string">"createdAt">>;
class="keyword">class="keyword">function updateUser(id: number, updates: UserUpdate) {
class="keyword">class="comment">// All fields are optional except id and createdAt
class="keyword">class="keyword">return db.user.update({ where: { id }, data: updates });
}
Required and Readonly
app.ts
class="keyword">interface Config {
apiUrl?: string;
timeout?: number;
retries?: number;
}
class="keyword">class="comment">// Make all fields required
class="keyword">type RequiredConfig = Required;
class="keyword">class="comment">// Make all fields class="keyword">readonly
class="keyword">type ImmutableConfig = Readonly;
class="keyword">class="comment">// Combine utilities
class="keyword">type StrictConfig = Required>;
Advanced Hooks Typing
Type-safe custom hooks.
Generic useLocalStorage
app.ts
class="keyword">class="keyword">function useLocalStorage(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => class="keyword">void] {
class="keyword">class="keyword">const [storedValue, setStoredValue] = useState(() => {
class="keyword">try {
class="keyword">class="keyword">const item = window.localStorage.getItem(key);
class="keyword">class="keyword">return item ? JSON.parse(item) : initialValue;
} class="keyword">catch (error) {
console.error(error);
class="keyword">class="keyword">return initialValue;
}
});
class="keyword">class="keyword">const setValue = (value: T | ((prev: T) => T)) => {
class="keyword">try {
class="keyword">class="keyword">const valueToStore =
value class="keyword">instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} class="keyword">catch (error) {
console.error(error);
}
};
class="keyword">class="keyword">return [storedValue, setValue];
}
class="keyword">class="comment">// Usage with class="keyword">type inference
class="keyword">class="keyword">const [user, setUser] = useLocalStorageclass="keyword">null>(class="keyword">class="string">"user", class="keyword">null);
class="keyword">class="keyword">const [theme, setTheme] = useLocalStorage<class="keyword">class="string">"light" | class="keyword">class="string">"dark">(class="keyword">class="string">"theme", class="keyword">class="string">"light");
Typed Context
app.ts
class="keyword">interface AuthContextType {
user: User | class="keyword">null;
login: (email: string, password: string) => Promise<class="keyword">void>;
logout: () => class="keyword">void;
isLoading: boolean;
}
class="keyword">class="keyword">const AuthContext = createContextclass="keyword">undefined>(class="keyword">undefined);
class="keyword">export class="keyword">class="keyword">function AuthProvider({ children }: { children: React.ReactNode }) {
class="keyword">class="keyword">const [user, setUser] = useStateclass="keyword">null>(class="keyword">null);
class="keyword">class="keyword">const [isLoading, setIsLoading] = useState(false);
class="keyword">class="keyword">const login = class="keyword">class="keyword">async (email: string, password: string) => {
setIsLoading(true);
class="keyword">try {
class="keyword">class="keyword">const user = class="keyword">class="keyword">await authService.login(email, password);
setUser(user);
} class="keyword">finally {
setIsLoading(false);
}
};
class="keyword">class="keyword">const logout = () => {
setUser(class="keyword">null);
authService.logout();
};
class="keyword">class="keyword">return (
{children}
);
}
class="keyword">class="comment">// Type-safe hook
class="keyword">export class="keyword">class="keyword">function useAuth() {
class="keyword">class="keyword">const context = useContext(AuthContext);
class="keyword">class="keyword">if (context === class="keyword">undefined) {
class="keyword">throw class="keyword">new Error(class="keyword">class="string">"useAuth must be used within AuthProvider");
}
class="keyword">class="keyword">return context;
}
Type Guards
Runtime type checking with type guards.
app.ts
class="keyword">interface Cat {
class="keyword">type: class="keyword">class="string">"cat";
meow: () => class="keyword">void;
}
class="keyword">interface Dog {
class="keyword">type: class="keyword">class="string">"dog";
bark: () => class="keyword">void;
}
class="keyword">type Animal = Cat | Dog;
class="keyword">class="comment">// Type guard
class="keyword">class="keyword">function isCat(animal: Animal): animal is Cat {
class="keyword">class="keyword">return animal.class="keyword">type === class="keyword">class="string">"cat";
}
class="keyword">class="keyword">function makeSound(animal: Animal) {
class="keyword">class="keyword">if (isCat(animal)) {
animal.meow(); class="keyword">class="comment">// TypeScript knows it's a Cat
} class="keyword">class="keyword">else {
animal.bark(); class="keyword">class="comment">// TypeScript knows it's a Dog
}
}
class="keyword">class="comment">// Generic class="keyword">type guard
class="keyword">class="keyword">function isType(
value: unknown,
check: (value: unknown) => boolean
): value is T {
class="keyword">class="keyword">return check(value);
}
class="keyword">class="comment">// Usage
class="keyword">class="keyword">const isString = (value: unknown): value is string => class="keyword">typeof value === class="keyword">class="string">"string";
class="keyword">class="keyword">const isNumber = (value: unknown): value is number => class="keyword">typeof value === class="keyword">class="string">"number";
Best Practices
1. Use unknown over any
app.ts
class="keyword">class="comment">// ❌ Avoid
class="keyword">class="keyword">function process(data: any) {
class="keyword">class="keyword">return data.value;
}
class="keyword">class="comment">// ✅ Better
class="keyword">class="keyword">function process(data: unknown) {
class="keyword">class="keyword">if (class="keyword">typeof data === class="keyword">class="string">"object" && data !== class="keyword">null && class="keyword">class="string">"value" in data) {
class="keyword">class="keyword">return (data as { value: string }).value;
}
}
2. Leverage Type Inference
app.ts
class="keyword">class="comment">// ❌ Redundant
class="keyword">class="keyword">const users: User[] = class="keyword">class="keyword">await fetchUsers();
class="keyword">class="comment">// ✅ Let TypeScript infer
class="keyword">class="keyword">const users = class="keyword">class="keyword">await fetchUsers(); class="keyword">class="comment">// Returns User[]
3. Use Const Assertions
app.ts
class="keyword">class="comment">// ✅ Narrow types
class="keyword">class="keyword">const routes = {
home: class="keyword">class="string">"/",
about: class="keyword">class="string">"/about",
contact: class="keyword">class="string">"/contact",
} as class="keyword">class="keyword">const;
class="keyword">type Route = (class="keyword">typeof routes)[keyof class="keyword">typeof routes];
class="keyword">class="comment">// Type: class="keyword">class="string">'/' | class="keyword">class="string">'/about' | class="keyword">class="string">'/contact'
Conclusion
Advanced TypeScript patterns transform how you build React applications. Generics provide reusability, discriminated unions enable type-safe state, conditional types add flexibility, and utility types reduce boilerplate.
Master these patterns to catch more bugs at compile time, improve IDE support, and create more maintainable applications.
Happy typing! 💪