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.

  • Frontend
  • $typescript-react-types
  • 237 lines
  • 2 files
  • ~2,250 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- typescript-react-types

Installs to ~/.agents/skills/typescript-react-types/ for every project on this machine.

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

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.

~/.agents/skills/typescript-react-types/SKILL.md

--- name: typescript-react-types description: Types React 18 and 19 components precisely with TypeScript - props extending native elements via ComponentProps, children and render props, ref as a… license: MIT ---

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

  1. Read AGENTS.md, tsconfig.json and package.json. Confirm "strict": true, "jsx": "react-jsx", and the versions of react and @types/react (they must match majors). React 19 types changed refs, JSX, and ReactElement defaults.
  2. Look at two existing components to copy the repo’s conventions (type vs interface, Props naming, where shared types live).
  3. Run npx tsc --noEmit first 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>
  );
}
NeedType
Everything a native element acceptsReact.ComponentProps<'button'> (includes ref in React 19)
Same, explicitly without refReact.ComponentPropsWithoutRef<'button'>
Props of another componentReact.ComponentProps<typeof Select>
Override a native prop’s type`Omit<React.ComponentProps<‘input’>, ‘onChange’
Anything renderableReact.ReactNode
Exactly one element (for cloneElement/slot APIs)React.ReactElement<{ className?: string }>
Render propchildren: (state: { open: boolean }) => React.ReactNode
Component passed as a propReact.ComponentType<IconProps> or React.ElementType
Inline styles with CSS variablesstyle={{ '--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 useDeclaration
DOM nodeuseRef<HTMLDivElement>(null), then ref.current?.
Mutable instance value (timer id)`useRef<number
Imperative handleref: React.Ref<DialogHandle> prop + useImperativeHandle(ref, () => ({ open, close }), [])
React 18 codeforwardRef<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), not e.target (typed as EventTarget).
  • 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 JSX namespace is gone: replace JSX.Element with React.JSX.Element (or React.ReactNode for return types of things that can return null).
  • useRef() with no argument is an error; RefObject.current is now mutable and MutableRefObject is deprecated.
  • ReactElement['props'] defaults to unknown; 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.ReactNode on a component that ignores children silently accepts them.
  • Record<string, T> index access returns T, not T | undefined, unless noUncheckedIndexedAccess is on; enable it for new code.
  • as casts 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 --noEmit passes with no new any, @ts-ignore or non-null assertions
  • Misuse examples fail with @ts-expect-error and 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem