一键导入
react-types
TypeScript + React typing patterns
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
TypeScript + React typing patterns
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Audit a project against a canon's rules and checklist. Read-only — produces prioritized report without fixing. Works with any canon (nextjs, sql, typescript, etc.).
Lens home base - status, help, and setup
Plan and build a new feature with quality gates.
Simple changes done right. Make the change, clean up after yourself, report what happened.
Review against canons + quality gate, fix findings, verify. Claude-native — no external models.
Plan and improve existing code with quality gates.
| name | react-types |
| description | TypeScript + React typing patterns |
Applying Matt Pocock's TypeScript expertise from "Total TypeScript" and his React TypeScript patterns when typing components, hooks, events, refs, context, and generic components. Auto-triggers on .tsx files, React component typing, hooks with generics, and event handler signatures.
Use interface for exported/shared component props. Inline types for one-off internals.
// Not this: inline type for a shared component
function Button({ label, onClick }: { label: string; onClick: () => void }) {
return <button onClick={onClick}>{label}</button>;
}
// This: interface for anything reusable or exported
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
function Button({ label, onClick, disabled }: ButtonProps) {
return <button onClick={onClick} disabled={disabled}>{label}</button>;
}
Always type children as ReactNode -- it covers strings, elements, fragments, portals, null, and arrays.
// Not this // This
interface CardProps { interface CardProps {
children: JSX.Element; children: ReactNode;
} }
Plain typed functions are preferred over React.FC<Props>. FC adds implicit children (pre-React 18), obscures the return type, and blocks generics.
// Not this
const Greeting: React.FC<GreetingProps> = ({ name }) => {
return <h1>Hello, {name}</h1>;
};
// This
function Greeting({ name }: GreetingProps) {
return <h1>Hello, {name}</h1>;
}
// FC blocks generic components -- you can't do this with FC
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
// ReactNode: anything renderable. Use for children and flexible slots.
interface LayoutProps {
header: ReactNode;
children: ReactNode;
}
// ReactElement: a concrete element. Use when you need cloneElement.
function Wrapper({ element }: { element: ReactElement }) {
return cloneElement(element, { className: 'wrapped' });
}
// JSX.Element: narrowest. Rarely needed directly.
// Prefer ReactElement when you need an element type.
Rule: Start with ReactNode. Narrow to ReactElement only for cloneElement or element inspection.
Use the specific React event type with the correct HTML element generic.
// Not this
const handleClick = (e: any) => { /* ... */ };
// This
import { MouseEvent, ChangeEvent, FormEvent, KeyboardEvent } from 'react';
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); // e.currentTarget typed as HTMLButtonElement
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; // string, fully typed
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { /* ... */ }
};
For callback props, use React's handler type aliases:
interface InputProps {
onChange: ChangeEventHandler<HTMLInputElement>;
onBlur: FocusEventHandler<HTMLInputElement>;
}
The null in the initial value determines whether the ref is mutable or read-only.
// DOM ref: pass null -- React manages .current (readonly RefObject)
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />;
}
// Mutable ref: pass a value -- you manage .current (MutableRefObject)
function Timer() {
const intervalRef = useRef<number>(0);
useEffect(() => {
intervalRef.current = window.setInterval(() => { /* tick */ }, 1000);
return () => clearInterval(intervalRef.current);
}, []);
}
The rule: useRef<T>(null) when React writes .current (DOM elements). useRef<T>(value) when you write .current (instance variables).
Let inference handle simple cases. Explicit generics for complex or initially-empty state.
// Inference works -- no annotation needed
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
// Explicit generic: initial value doesn't capture the full type
const [user, setUser] = useState<User | null>(null);
// Discriminated union state
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function useAsync<T>() {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });
if (state.status === 'success') {
state.data; // T -- narrowed, accessible only here
}
return [state, setState] as const;
}
The common mistake is createContext(undefined) then asserting non-null everywhere. Solve it with a custom hook.
// Not this: undefined default forces null checks everywhere
const UserContext = createContext<User | undefined>(undefined);
function Profile() {
const user = useContext(UserContext); // User | undefined every time
return <p>{user?.name}</p>;
}
// This: custom hook throws if used outside provider
const UserContext = createContext<User | null>(null);
function useUser(): User {
const user = useContext(UserContext);
if (user === null) {
throw new Error('useUser must be used within a UserProvider');
}
return user; // User, guaranteed
}
function UserProvider({ user, children }: { user: User; children: ReactNode }) {
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}
function Profile() {
const user = useUser();
return <p>{user.name}</p>; // no undefined, no null checks
}
Generic components preserve type relationships between props.
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
}
function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
return (
<select
value={String(value)}
onChange={(e) => onChange(options[e.target.selectedIndex])}
>
{options.map((option, i) => (
<option key={i} value={String(option)}>{getLabel(option)}</option>
))}
</select>
);
}
// T inferred from options
<Select
options={countries}
value={selected} // Country
onChange={(c) => setSelected(c)} // c is Country
getLabel={(c) => c.name} // c is Country
/>
Constrain with extends when you need specific properties:
interface TableProps<T extends { id: string | number }> {
rows: T[];
columns: (keyof T)[];
onRowClick: (row: T) => void;
}
function Table<T extends { id: string | number }>({
rows, columns, onRowClick
}: TableProps<T>) {
return (
<table><tbody>
{rows.map((row) => (
<tr key={row.id} onClick={() => onRowClick(row)}>
{columns.map((col) => <td key={String(col)}>{String(row[col])}</td>)}
</tr>
))}
</tbody></table>
);
}
Extract props from existing components instead of redeclaring them.
import { ComponentProps } from 'react';
// Extend native element props
interface IconButtonProps extends ComponentProps<'button'> {
icon: ReactNode;
label: string;
}
function IconButton({ icon, label, ...buttonProps }: IconButtonProps) {
return <button aria-label={label} {...buttonProps}>{icon}</button>;
}
// Extract from custom components
type DialogProps = ComponentProps<typeof Dialog>;
The polymorphic as prop pattern:
interface BoxProps<T extends React.ElementType> {
as?: T;
children: ReactNode;
}
type PolymorphicProps<T extends React.ElementType> =
BoxProps<T> & Omit<ComponentProps<T>, keyof BoxProps<T>>;
function Box<T extends React.ElementType = 'div'>({
as, children, ...rest
}: PolymorphicProps<T>) {
const Component = as || 'div';
return <Component {...rest}>{children}</Component>;
}
<Box as="a" href="/home">Link</Box> // href valid
<Box as="button" onClick={fn}>Click</Box> // onClick valid
<Box>Just a div</Box> // defaults to div
forwardRef requires a specific generic signature. React 19 simplifies this.
// React 18: forwardRef<Element, Props>
interface InputProps {
label: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
function Input({ label, error, ...props }, ref) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}
);
const inputRef = useRef<HTMLInputElement>(null);
<Input ref={inputRef} label="Email" />
// React 19: ref is just a prop -- no wrapper needed
interface InputPropsR19 {
label: string;
error?: string;
ref?: React.Ref<HTMLInputElement>;
}
function Input({ label, error, ref, ...props }: InputPropsR19) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}
any in event handlers -- Defeats TypeScript. You lose autocomplete on e.target, e.currentTarget, and element-specific properties.children: JSX.Element -- Breaks on strings, numbers, null, fragments. Always use ReactNode.React.FC everywhere -- Adds implicit children (pre-18), blocks generics, obscures return types.as -- useContext(Ctx) as T silences the compiler but crashes at runtime if the provider is missing. Use a custom hook with a runtime guard.useState<string>('') is redundant. Only use explicit generics when the initial value is narrower than the intended type.useRef<T>(null!) -- The non-null assertion hack creates a mutable ref and bypasses safety. Stick with null for DOM refs.| Situation | Approach |
|---|---|
| Shared/exported component props | interface with descriptive name |
| Internal one-off component | Inline type in destructuring |
| Children slot | ReactNode |
| Cloning/inspecting elements | ReactElement |
| Event handler prop | ChangeEventHandler<HTMLInputElement> |
| Event handler inline | (e: ChangeEvent<HTMLInputElement>) => void |
| DOM ref | useRef<HTMLElement>(null) |
| Instance variable ref | useRef<T>(initialValue) |
| Simple initial state | Let inference work |
| Nullable or union state | Explicit useState<T | null>(null) |
| Context consumed by many | Custom hook with runtime guard |
| Type-safe list/table | Generic component with extends constraint |
| Wrapping native elements | ComponentProps<'element'> |
Polymorphic as prop | ComponentProps<T> with Omit |
| Exposing ref (React 18) | forwardRef<Element, Props> |
| Exposing ref (React 19) | ref?: React.Ref<Element> as a prop |
When reviewing TypeScript React code, verify:
interface for exported components, not inline typeschildren typed as ReactNode, not JSX.Element or stringReact.FC -- plain functions with typed propsuseRef<Element>(null), mutable refs use useRef<T>(value)useState only has explicit generics when inference is insufficientextends to constrain type parametersComponentProps<'element'> not manual re-typingforwardRef uses the correct <Element, Props> generic orderany in event handlers, context values, or hook returnsas type assertions to work around missing providers