Typing Props and Children

Props are one object, so type them with one object type — interface or type alias; the choice barely matters. Annotate the parameter and inference does the rest. Two React 7,897 names cover most cases: ReactNode is anything React can render (elements, strings, numbers, arrays, null, undefined, false) and is what children should almost always be; ComponentProps<'button'> is a built-in element's full prop type, so a wrapper inherits every native attribute — children included.

A button wrapper that accepts every native button propJavaScript
import type { ComponentProps, ReactNode } from 'react';
type ButtonProps = ComponentProps<'button'> &
  { variant?: 'primary' | 'ghost'; icon?: ReactNode };
export function Button({ variant = 'primary', icon, children, ...rest }: ButtonProps) {
  return <button className={`btn btn-${variant}`} {...rest}>{icon}{children}</button>;
}
export const ok = <Button type="submit" onClick={() => {}} variant="ghost">Save</Button>;
export const bad = <Button type="sumbit">Save</Button>;
Output
src/props.tsx(10,28): error TS2820: Type '"sumbit"' is not assignable to type
  '"button" | "reset" | "submit" | undefined'. Did you mean '"submit"'?

With no native element to borrow from, add children: ReactNode yourself or wrap the props type in PropsWithChildren<P>; for a prop holding another component's props, use ComponentProps<typeof Button>.

Props that come in combinations

When one prop's presence depends on another's value, model the props as a union of object types rather than making everything optional. The compiler narrows inside the component and rejects illegal combinations.

Discriminated union propsCSS
type AlertProps =
  | { kind: 'error'; message: string; retry: () => void }
  | { kind: 'info'; message: string };
export function Alert(props: AlertProps) {
  return <div role="alert">{props.message}
    {props.kind === 'error' && <button onClick={props.retry}>Retry</button>}</div>;
}

<Alert kind="info" message="Saved." retry={...} /> is now TS2322, Property 'retry' does not exist on type 'IntrinsicAttributes & { kind: "info"; message: string; }', while props.retry inside the guard type-checks because the kind test narrowed props. Destructuring { kind, retry } in the parameter list loses that narrowing.