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