//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.
The complete SKILL.md content you can copy into .claude/skills/typescript-react-patterns/SKILL.md:
---
name: typescript-react-patterns
description: "Advanced TypeScript patterns for React and Next.js components. Use when asked to: type this component, TypeScript help, generic component, typing props, discriminated unions, utility types, typing hooks, typing server components."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# TypeScript React Patterns
You are a TypeScript expert specializing in React and Next.js patterns. Provide the most precise, strict types for every scenario.
## Core Principles
1. **Prefer narrow types over broad ones** - Use string literals over string, specific objects over Record
2. **Use discriminated unions for conditional rendering** - Never optional-chain your way through variant props
3. **Derive types from data** - Use typeof, ReturnType, and Zod inference instead of manual type declarations
4. **Strict mode always** - Enable strict: true and noUncheckedIndexedAccess: true
## Pattern Library
### 1. Polymorphic Component (as prop)
```tsx
type PolymorphicProps<E extends React.ElementType> = \{
as?: E;
children: React.ReactNode;
\} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">(\{
as,
children,
...props
\}: PolymorphicProps<E>) \{
const Component = as ?? "div";
return <Component \{...props\}>\{children\}</Component>;
\}
// Usage - fully typed
<Box as="a" href="/about">Link</Box> // href is valid
<Box as="button" onClick=\{handleClick\}>Go</Box> // onClick is valid// Instead of optional props that depend on each other:
// BAD
type BadProps = \{ variant?: "link"; href?: string; onClick?: () => void \};
// GOOD - discriminated union
type ButtonProps =
| \{ variant: "button"; onClick: () => void; href?: never \}
| \{ variant: "link"; href: string; onClick?: never \}
| \{ variant: "submit"; onClick?: never; href?: never \};
function Action(props: ButtonProps) \{
switch (props.variant) \{
case "button":
return <button onClick=\{props.onClick\}>Click</button>;
case "link":
return <a href=\{props.href\}>Link</a>;
case "submit":
return <button type="submit">Submit</button>;
\}
\}type ListProps<T> = \{
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
\};
function List<T>(\{ items, renderItem, keyExtractor, emptyMessage \}: ListProps<T>) \{
if (items.length === 0) \{
return <p>\{emptyMessage ?? "No items"\}</p>;
\}
return (
<ul>
\{items.map((item, i) => (
<li key=\{keyExtractor(item)\}>\{renderItem(item, i)\}</li>
))\}
</ul>
);
\}
// Usage - T is inferred from items
<List
items=\{users\}
renderItem=\{(user) => <span>\{user.name\}</span>\} // user is typed as User
keyExtractor=\{(user) => user.id\}
/>import type \{ ComponentProps, ComponentRef \} from "react";
// Extract props from any component
type InputProps = ComponentProps<"input">;
type ButtonProps = ComponentProps<typeof Button>;
// Extract ref type
type InputRef = ComponentRef<"input">; // HTMLInputElement
// Pick specific props
type PartialInputProps = Pick<ComponentProps<"input">, "value" | "onChange" | "placeholder">;// Return tuple (like useState)
function useToggle(initial = false) \{
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, \{ toggle, setTrue, setFalse \}] as const;
\}
// Return type: readonly [boolean, \{ toggle, setTrue, setFalse \}]
// Generic hook
function useLocalStorage<T>(key: string, initialValue: T) \{
const [stored, setStored] = useState<T>(() => \{
if (typeof window === "undefined") return initialValue;
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
\});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => \{
setStored((prev) => \{
const next = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(next));
return next;
\});
\},
[key]
);
return [stored, setValue] as const;
\}// Server Component props - params and searchParams are Promises in Next.js 15+
type PageProps = \{
params: Promise<\{ slug: string \}>;
searchParams: Promise<\{ [key: string]: string | string[] | undefined \}>;
\};
export default async function Page(\{ params, searchParams \}: PageProps) \{
const \{ slug \} = await params;
const \{ q \} = await searchParams;
// ...
\}
// Layout props
type LayoutProps = \{
children: React.ReactNode;
params: Promise<\{ slug: string \}>;
\};
export default async function Layout(\{ children, params \}: LayoutProps) \{
const \{ slug \} = await params;
return <div>\{children\}</div>;
\}// Server Action with typed state
type FormState = \{
errors?: \{
name?: string[];
email?: string[];
\};
message?: string;
success: boolean;
\};
export async function createUser(
prevState: FormState,
formData: FormData
): Promise<FormState> \{
// validate and process
return \{ success: true, message: "User created" \};
\}
// Client component using the action
"use client";
import \{ useActionState \} from "react";
function Form() \{
const [state, action, pending] = useActionState(createUser, \{
success: false,
\});
// state is typed as FormState
\}// Specific event types instead of generic React.SyntheticEvent
function Form() \{
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => \{
console.log(e.target.value); // string
\};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => \{
e.preventDefault();
const formData = new FormData(e.currentTarget);
\};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => \{
if (e.key === "Enter") submit();
\};
return (
<form onSubmit=\{handleSubmit\}>
<input onChange=\{handleChange\} onKeyDown=\{handleKeyDown\} />
</form>
);
\}// Make specific keys required
type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Make specific keys optional
type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Deep partial
type DeepPartial<T> = \{
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
\};
// Strict omit (errors if key doesn't exist)
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
// Extract union member
type ExtractMember<T, U> = T extends U ? T : never;// Type guard function
function isError(result: SuccessResult | ErrorResult): result is ErrorResult \{
return "error" in result;
\}
// Exhaustive switch
function assertNever(value: never): never \{
throw new Error(`Unexpected value: ${value}`);
\}
function handleStatus(status: "idle" | "loading" | "success" | "error") \{
switch (status) \{
case "idle": return null;
case "loading": return <Spinner />;
case "success": return <Done />;
case "error": return <Error />;
default: return assertNever(status); // compile error if case missed
\}
\}any - Use unknown and narrow, or use a genericas for type assertions - Use type guards or discriminated unionssatisfies over as const when you need both inference and validation
## Working Example
### Example 1: User asks "How do I type a component that can be a button or link?"
**User prompt:** "I need a Button component that renders as a link when href is provided."
**Skill-guided response would produce:**
```tsx
type ButtonBaseProps = {
children: React.ReactNode;
size?: "sm" | "md" | "lg";
variant?: "primary" | "secondary" | "ghost";
};
type ButtonAsButton = ButtonBaseProps &
Omit<React.ComponentPropsWithoutRef<"button">, keyof ButtonBaseProps> & {
href?: never;
};
type ButtonAsLink = ButtonBaseProps &
Omit<React.ComponentPropsWithoutRef<"a">, keyof ButtonBaseProps> & {
href: string;
};
type ButtonProps = ButtonAsButton | ButtonAsLink;
function Button(props: ButtonProps) {
if (props.href !== undefined) {
const { size, variant, ...rest } = props;
return <a className={getStyles(size, variant)} {...rest} />;
}
const { size, variant, ...rest } = props;
return <button className={getStyles(size, variant)} {...rest} />;
}
Skill-guided response would use Zod inference:
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "user", "moderator"]),
});
type User = z.infer<typeof UserSchema>;
// Now User type is derived from the schema - single source of truthThis skill provides Claude with a comprehensive pattern library covering:
any, no as)mkdir -p .claude/skills/typescript-react-patterns
# Paste the Recipe content into .claude/skills/typescript-react-patterns/SKILL.mdComponentPropsWithoutRef by default. Only use ComponentPropsWithRef when you explicitly need to forward refs.satisfies validates but the variable retains its inferred type, not the checked type.| Approach | When to Use |
|---|---|
| JSDoc types | Projects that cannot adopt TypeScript |
| io-ts | Runtime validation with fp-ts integration |
| Valibot | Smaller bundle alternative to Zod |
| ArkType | Faster schema validation with TypeScript-native syntax |
string)z.infer, ReturnType, typeof)strict: true and noUncheckedIndexedAccess: true)type ButtonProps =
| { variant: "button"; onClick: () => void; href?: never }
| { variant: "link"; href: string; onClick?: never }
| { variant: "submit"; onClick?: never; href?: never };never)href to a "button" varianttype ListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}T is inferred automatically from the items array passed by the callertype PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export default async function Page({ params, searchParams }: PageProps) {
const { slug } = await params;
const { q } = await searchParams;
}params and searchParams are Promise types in Next.js 15+ and must be awaitedReact.memoT is not preserved through the memo boundaryComponentPropsWithoutRef<"input"> -- extracts props without ref (use by default)ComponentPropsWithRef<"input"> -- includes the ref typeComponentPropsWithRef when you explicitly need to forward refstype PolymorphicProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">({
as, children, ...props
}: PolymorphicProps<E>) {
const Component = as ?? "div";
return <Component {...props}>{children}</Component>;
}ashref is valid when as="a")WithRequired<T, K> -- make specific keys requiredWithOptional<T, K> -- make specific keys optionalDeepPartial<T> -- recursively make all keys optionalStrictOmit<T, K> -- Omit that errors if the key does not existExtractMember<T, U> -- extract a specific member from a unionsatisfies validates that a value matches a type but retains the inferred type (does not narrow)as const makes the value deeply readonly with literal typessatisfies when you need both inference and validationtype FormState = {
errors?: { name?: string[]; email?: string[] };
message?: string;
success: boolean;
};
export async function createUser(
prevState: FormState,
formData: FormData
): Promise<FormState> {
return { success: true, message: "User created" };
}prevState (typed as FormState) and formData (FormData)Promise<FormState>, matching the initial state shapeany disables all type checking and lets bugs slip throughunknown and narrow with type guards instead of anyas assertions bypass the type checker and can mask errorsReviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥