TypeScript React Types skill for Codex
Props that reject misuse and never need a cast. Codex types React components from the platform instead of hand-written attribute lists, using ComponentProps, React 19 ref props and precise event handler aliases. It models invalid prop combinations as discriminated unions, writes generic lists and polymorphic as components whose types infer at the call site, guards context without non-null assertions, and locks the API with type tests.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- typescript-react-types Installs to ~/.agents/skills/typescript-react-types/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install typescript-react-types The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project typescript-react-types Installs to .agents/skills/typescript-react-types/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/typescript-react-types.zipunzip -o typescript-react-types.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) typescript-react-types PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $typescript-react-types, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Types React 18 and 19 components precisely with TypeScript - props extending native elements via ComponentProps, children and render props, ref as a prop, event handlers, useState/useRef/useReducer/useActionState, context without non-null assertions, generic components, discriminated-union props, and polymorphic "as" components - and replaces any, casts and React.FC habits with types that catch real misuse. Use when writing or reviewing typed React components, when prop or ref types fight back, when upgrading @types/react to 19, or when building a typed component library.
Use it when
- Writing or reviewing typed React components and hooks
- Prop, ref or event types produce errors or require casts
- Building generic, polymorphic or variant-heavy components for a library
- Upgrading @types/react to version 19
Use something else when
- The hard part is advanced type-level programming beyond React: use typescript-generics
- You are turning on strict mode across a codebase: use typescript-strict-mode
How to check the result
tsc --noEmit passes with no new any or non-null assertions, and @ts-expect-error type tests prove that invalid prop combinations are rejected.
The mistake it prevents
Spreading rest props after your own props without thinking, or casting props with as, which hides the exact misuse the types were meant to catch.
Good component types reject wrong usage at the call site with a readable error, accept
every legitimate use without casts, and come from the platform (ComponentProps<'button'>)
rather than hand-copied attribute lists. No any, no as unknown as, no ! unless an
invariant is proven a line above.
Before typing anything
- Read
AGENTS.md,tsconfig.jsonandpackage.json. Confirm"strict": true,"jsx": "react-jsx", and the versions ofreactand@types/react(they must match majors). React 19 types changed refs,JSX, andReactElementdefaults. - Look at two existing components to copy the repo’s conventions (type vs interface,
Propsnaming, where shared types live). - Run
npx tsc --noEmitfirst so you know which errors predate your change.
Props basics
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'secondary' | 'ghost';
loading?: boolean;
};
export function Button({ variant = 'primary', loading = false, disabled, children, ...rest }: ButtonProps) {
return (
<button {...rest} disabled={disabled || loading} data-variant={variant} aria-busy={loading}>
{children}
</button>
);
}
| Need | Type |
|---|---|
| Everything a native element accepts | React.ComponentProps<'button'> (includes ref in React 19) |
| Same, explicitly without ref | React.ComponentPropsWithoutRef<'button'> |
| Props of another component | React.ComponentProps<typeof Select> |
| Override a native prop’s type | `Omit<React.ComponentProps<‘input’>, ‘onChange’ |
| Anything renderable | React.ReactNode |
| Exactly one element (for cloneElement/slot APIs) | React.ReactElement<{ className?: string }> |
| Render prop | children: (state: { open: boolean }) => React.ReactNode |
| Component passed as a prop | React.ComponentType<IconProps> or React.ElementType |
| Inline styles with CSS variables | style={{ '--gap': '8px' } as React.CSSProperties} |
Prefer plain function declarations over React.FC; FC adds nothing since children are
no longer implicit and it makes generics awkward. Put defaults in destructuring, not
defaultProps (removed for function components in React 19).
Refs
React 19: ref is a normal prop for function components; forwardRef is unnecessary.
function TextField({ label, ref, ...rest }: React.ComponentProps<'input'> & { label: string }) {
const id = React.useId();
return (<><label htmlFor={id}>{label}</label><input id={id} ref={ref} {...rest} /></>);
}
const inputRef = React.useRef<HTMLInputElement>(null); // RefObject<HTMLInputElement | null>
inputRef.current?.focus();
| Ref use | Declaration |
|---|---|
| DOM node | useRef<HTMLDivElement>(null), then ref.current?. |
| Mutable instance value (timer id) | `useRef<number |
| Imperative handle | ref: React.Ref<DialogHandle> prop + useImperativeHandle(ref, () => ({ open, close }), []) |
| React 18 code | forwardRef<HTMLInputElement, Props>((props, ref) => ...); set displayName |
Events
function Search({ onSearch }: { onSearch: (q: string) => void }) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => onSearch(e.currentTarget.value);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
onSearch(String(data.get('q') ?? ''));
};
return <form onSubmit={handleSubmit}><input name="q" onChange={handleChange} /></form>;
}
- Use
e.currentTarget(typed as the element the handler is on), note.target(typed asEventTarget). - When passing handlers down, type the prop with the handler alias:
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>. - Inline handlers infer their event type; only annotate handlers declared separately.
- Expose domain callbacks (
onSelect(id: string)) instead of raw DOM events when the consumer does not need the event.
Hooks
const [user, setUser] = React.useState<User | null>(null); // annotate when initial value is narrower
const [items, setItems] = React.useState<Item[]>([]); // [] alone infers never[]
type Action =
| { type: 'added'; item: Item }
| { type: 'removed'; id: string }
| { type: 'cleared' };
function reducer(state: Item[], action: Action): Item[] {
switch (action.type) {
case 'added': return [...state, action.item];
case 'removed': return state.filter((i) => i.id !== action.id);
case 'cleared': return [];
default: { const _exhaustive: never = action; return state; }
}
}
// React 19 form actions
type FormState = { error?: string; ok?: boolean };
const [state, formAction, pending] = React.useActionState<FormState, FormData>(
async (_prev, formData) => save(formData), {},
);
Custom hooks returning tuples need as const, or callers get a union array:
return [value, setValue] as const;. Return an object when there are more than two values.
Context without !
type Auth = { user: User; signOut(): Promise<void> };
const AuthContext = React.createContext<Auth | null>(null);
export function useAuth(): Auth {
const ctx = React.use(AuthContext); // or useContext in React 18
if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>');
return ctx;
}
Generic components
type ListProps<T> = {
items: readonly T[];
getKey: (item: T) => React.Key;
renderItem: (item: T) => React.ReactNode;
onSelect?: (item: T) => void;
};
export function List<T>({ items, getKey, renderItem, onSelect }: ListProps<T>) {
return <ul>{items.map((it) => (
<li key={getKey(it)} onClick={() => onSelect?.(it)}>{renderItem(it)}</li>
))}</ul>;
}
// In .tsx, arrow generics need a trailing comma: const List = <T,>(props: ListProps<T>) => ...
T is inferred from items, so onSelect receives the correct item type at the call site.
Discriminated-union props
Make invalid combinations unrepresentable instead of documenting them:
type LinkOrButton =
| ({ href: string } & Omit<React.ComponentProps<'a'>, 'href'>)
| ({ href?: never } & React.ComponentProps<'button'>);
function Action(props: LinkOrButton) {
if (props.href !== undefined) return <a {...props} />;
return <button type="button" {...props} />;
}
Destructuring before narrowing breaks narrowing; check the discriminant on props first.
Polymorphic as components
type PolymorphicProps<E extends React.ElementType, P = object> =
P & { as?: E } & Omit<React.ComponentProps<E>, keyof P | 'as'>;
export function Text<E extends React.ElementType = 'span'>(
{ as, size = 'md', ...rest }: PolymorphicProps<E, { size?: 'sm' | 'md' | 'lg' }>,
) {
const Component = as ?? 'span';
return <Component data-size={size} {...rest} />;
}
<Text as="a" href="/docs">Docs</Text>; // href allowed
<Text as="label" htmlFor="email" />; // htmlFor allowed
// <Text href="/x" /> // error: span has no href
Polymorphic types are slow for the checker in large unions; for most design systems an
asChild slot pattern (Radix Slot) gives the same flexibility with simpler types.
React 19 type migration
- Global
JSXnamespace is gone: replaceJSX.ElementwithReact.JSX.Element(orReact.ReactNodefor return types of things that can return null). useRef()with no argument is an error;RefObject.currentis now mutable andMutableRefObjectis deprecated.ReactElement['props']defaults tounknown; add the props type parameter.- Codemods exist (
npx types-react-codemod@latest preset-19 ./src, needs network); review the diff rather than accepting it blindly.
Gotchas
{...rest}spread before your own props lets callers override them; spread after if your values must win (<button {...rest} type="button">).children?: React.ReactNodeon a component that ignores children silently accepts them.Record<string, T>index access returnsT, notT | undefined, unlessnoUncheckedIndexedAccessis on; enable it for new code.ascasts on props hide real bugs; if you need one, the type is wrong upstream.- Enums for variants block string literals from JSON and CMS data; use union literals.
Verification
npx tsc --noEmit
Lock in the API with type tests so refactors cannot loosen it:
import { expectTypeOf } from 'vitest';
expectTypeOf<React.ComponentProps<typeof Button>>().toHaveProperty('variant');
// @ts-expect-error: href is not valid on a span
<Text href="/x" />;
-
tsc --noEmitpasses with no newany,@ts-ignoreor non-null assertions - Misuse examples fail with
@ts-expect-errorand legitimate uses compile without casts - Editor hover on each exported component shows a readable props type
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "TypeScript React Types"
short_description: "Props that reject misuse and never need a cast"
default_prompt: "$typescript-react-types Type our Button so it accepts every native button prop plus a variant" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.