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

app.ts

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

app.ts

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

))}

{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.header}
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 (

class="keyword">type=class="keyword">class="string">"email"

value={state.email.value}

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

/>

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

class="keyword">class="string">"text-red-500">{state.email.error}

)}

class="keyword">type=class="keyword">class="string">"password"

value={state.password.value}

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

/>

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

class="keyword">class="string">"text-red-500">{state.password.error}

)}

);

}

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

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.