配 tsconfig 最低要求

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "jsx": "preserve"
  }
}

strict: true 是底线。noUncheckedIndexedAccessarr[i] 返回 T | undefined——避免运行时 cannot read property of undefined

函数组件的类型

type Props = {
  title: string;
  onClick?: () => void;
  children?: React.ReactNode;
};

function Card({ title, onClick, children }: Props) {
  return <div onClick={onClick}>{title}{children}</div>;
}

不要用 React.FC(2022 后官方不推荐):

  • 隐式加 children(即使你不用)
  • 不支持泛型
  • 显式写 Props 类型更直接

children 的类型

React.ReactNode         // 最常用,啥都能放(字符串、元素、null、数组)
React.ReactElement      // 只能是单个 React Element
JSX.Element             // 单个 JSX 表达式的结果

90% 时间用 React.ReactNode

事件类型

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};

const onClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... };
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); };
const onKey = (e: React.KeyboardEvent<HTMLInputElement>) => { ... };

不知道哪个?IDE 悬停 onChange 看 prop 类型——它一定写着 ChangeEventHandler<HTMLXxxElement>

ref 的类型

const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />

inputRef.current?.focus();   // optional chaining 因为初始 null

类型必须匹配 DOM 元素:HTMLInputElement / HTMLDivElement / HTMLButtonElement……

useState 的类型推断

const [n, setN] = useState(0);              // 推断为 number
const [user, setUser] = useState<User | null>(null);   // 需显式给
const [arr, setArr] = useState<string[]>([]);          // 显式给避免 never[]

陷阱useState([]) 推断为 never[],push 任何东西都报错。一定显式 useState<X[]>([])

props 中的事件回调

type Props = {
  onSelect: (id: string) => void;          // 推荐
  onClick: React.MouseEventHandler;        // 也行
  onChange: React.ChangeEventHandler<HTMLInputElement>;
};

第一种最常用、最清楚。

泛型组件

type ListProps<T> = {
  items: T[];
  render: (item: T) => React.ReactNode;
};

function List<T>({ items, render }: ListProps<T>) {
  return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;
}

<List items={users} render={(u) => u.name} />

T 由调用处自动推断。

受控 input 的两种风格

// 风格 1:每个 input 单独类型
<input value={text} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setText(e.target.value)} />

// 风格 2:依赖 React 自动推断
<input value={text} onChange={(e) => setText(e.target.value)} />   // ✅ 通常够了

通常风格 2 就行——IDE 会自动推。

Discriminated Union(鉴别联合)

特别适合 React props:

type Props =
  | { mode: 'view'; data: User }
  | { mode: 'edit'; data: User; onSave: (u: User) => void }
  | { mode: 'create'; onSave: (u: User) => void };

function UserPanel(props: Props) {
  if (props.mode === 'create') {
    // 这里 TS 知道没有 data
    return <form onSubmit={() => props.onSave(...)} />;
  }
}

props.data? 到处判空清晰太多。

Server Component vs Client 的类型

Server Component 默认是 async

// Next.js 15+:params 和 searchParams 都是 Promise
export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<{ id: string }>;
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const { id } = await params;
  const sp = await searchParams;
  const user = await db.user.findUnique({ where: { id } });
  return <UserCard user={user} />;
}

App Router 自动注入 params / searchParams。Next 14 及以前是同步对象;15+ 改成 Promise。

Client Component 不能 async——返回 JSX,类型同函数组件。

工具类型

ComponentProps<typeof Button>           // 拿 Button 的 props 类型
ComponentProps<'input'>                 // 拿 <input> 的所有原生 props
Omit<Props, 'onClick'>
Pick<Props, 'title' | 'desc'>
Required<Partial<X>>

继承原生元素 props

type ButtonProps = React.ComponentProps<'button'> & {
  variant: 'primary' | 'ghost';
};

这样 Button 自动支持所有原生 button 属性(onClickdisabledtype...)+ 你自定义的。

常见错误

  • 导出类型时少用 defaultexport type Props = ... 比 default 好——import { Props } 更清晰
  • as 类型断言绕过检查——只在你确信比 TS 更知情时用,否则是埋雷
  • any 是放弃治疗——unknown 一样能跳过类型检查但强制你后续 narrow
  • enum:默认不要用 enum,用 union literal type:
// ❌
enum Status { Loading, Done, Error }
// ✅
type Status = 'loading' | 'done' | 'error';

→ 下一篇 测试