Back to blog

Advanced TypeScript Patterns for React Applications

9 min readBy Mustafa Akkaya
#TypeScript#React#Advanced Patterns#Type Safety

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) => (

))}

{data.map((item) => (

key={item.id}

onClick={() => onRowClick?.(item)}

className="cursor-pointer hover:bg-gray-100"

>

{columns.map((col) => (

))}

))}

{col.header}

{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 (

type="email"

value={state.email.value}

onChange={(e) => setValue("email", e.target.value)}

/>

{state.email.touched && state.email.error && (

{state.email.error}

)}

type="password"

value={state.password.value}

onChange={(e) => setValue("password", e.target.value)}

/>

{state.password.touched && state.password.error && (

{state.password.error}

)}

);

}

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! 💪

akkaya.dev

Crafting exceptional digital experiences with cutting-edge web technologies and modern design principles.

Tech Stack

  • Next.js & React
  • TypeScript
  • Node.js & Express
  • TailwindCSS

© 2025 Mustafa Akkaya. All rights reserved.