Skip to content
第 19 章 前端 ⏱ 14 分钟阅读

第 19 章:TypeScript + React ​

学习目标 ​

  • 掌握 React 组件的 4 种类型定义
  • 熟练使用常见 hooks 的泛型
  • 处理事件、ref、children 类型
  • 避开 TypeScript 的 5 个常见坑

一、组件 Props 类型 ​

1. interface(推荐) ​

tsx
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'danger';
  children?: React.ReactNode;
}

function Button({ label, onClick, variant = 'primary', children }: ButtonProps) {
  return <button onClick={onClick} className={`btn btn-${variant}`}>{children ?? label}</button>;
}

2. type(联合/复杂类型) ​

tsx
type Status = 'loading' | 'success' | 'error';

interface CardProps {
  status: Status;
  data?: unknown;
}

3. 继承 HTML 属性 ​

tsx
import { ButtonHTMLAttributes } from 'react';

interface MyButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary';
}

function MyButton({ variant = 'primary', ...rest }: MyButtonProps) {
  return <button className={`btn btn-${variant}`} {...rest} />;
}

⚠️ 坑 1:不继承原生属性就拿不到 onClick、disabled 等标准 props。

4. 函数组件类型(React.FC) ​

tsx
const Card: React.FC<{ title: string }> = ({ title, children }) => (
  <div>
    <h3>{title}</h3>
    {children}
  </div>
);

⚠️ 坑 2:React 18 起 React.FC 不再隐式包含 children,且会让泛型变复杂。推荐用 interface + 普通函数。

二、children 类型 ​

tsx
interface Props {
  children: React.ReactNode;        // 最宽,任何可渲染内容
  // 或更精确:
  children: React.ReactElement;     // 必须是 React 元素
  // 或:
  renderItem: (item: Item) => React.ReactNode;   // 渲染函数
}

三、useState 类型 ​

tsx
// 自动推断
const [count, setCount] = useState(0);           // number
const [name, setName] = useState('');            // string

// 显式类型
const [user, setUser] = useState<User | null>(null);

// 联合类型 + 判别
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

const [state, setState] = useState<AsyncState<User>>({ status: 'idle' });

if (state.status === 'success') {
  console.log(state.data);      // ✅ 类型已收窄
}

四、useRef 类型 ​

tsx
// DOM 引用
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();     // ⚠️ 必须用 ?.

// 值 ref
const countRef = useRef<number>(0);
countRef.current = 1;

五、useReducer 类型 ​

tsx
interface State { count: number; }

type Action =
  | { type: 'inc' }
  | { type: 'dec' }
  | { type: 'set'; value: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'inc': return { count: state.count + 1 };
    case 'dec': return { count: state.count - 1 };
    case 'set': return { count: action.value };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });

⚠️ 坑 3:switch 必须穷举所有 action,加 default return state 防止漏处理。

六、事件类型 ​

tsx
function Form() {
  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    console.log(e.target.value);
  }

  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    console.log(e.currentTarget);
  }

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
  }

  function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') console.log('回车');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKey} />
      <button onClick={handleClick}>点</button>
    </form>
  );
}

七、自定义 Hook 类型 ​

tsx
function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {
  const [value, setValue] = useState<T>(() => {
    const saved = localStorage.getItem(key);
    return saved ? (JSON.parse(saved) as T) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

⚠️ 坑 4:返回元组时类型用 [T, (v: T) => void],不要写 any[]。

八、forwardRef 类型 ​

tsx
import { forwardRef, useImperativeHandle, useRef } from 'react';

interface MyInputRef { focus: () => void; clear: () => void; }
interface MyInputProps { defaultValue?: string; }

const MyInput = forwardRef<MyInputRef, MyInputProps>(
  ({ defaultValue }, ref) => {
    const innerRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => ({
      focus: () => innerRef.current?.focus(),
      clear: () => { if (innerRef.current) innerRef.current.value = ''; },
    }));

    return <input ref={innerRef} defaultValue={defaultValue} />;
  },
);

九、泛型组件 ​

tsx
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyOf: (item: T) => string | number;
}

function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyOf(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 使用
<List<User>
  items={users}
  renderItem={u => <span>{u.name}</span>}
  keyOf={u => u.id}
/>

十、常用工具类型 ​

tsx
type Partial<T> = { [K in keyof T]?: T[K] };      // 全可选
type Pick<T, K extends keyof T> = { [P in K]: T[P] };   // 选字段

type User = { id: number; name: string; age: number };
type UserPreview = Pick<User, 'id' | 'name'>;   // { id, name }

// React 提供的:
type PropsWithChildren<P> = P & { children?: React.ReactNode };

⚠️ 坑 5:Omit<HTMLProps, 'onClick'> 这种操作可以"剔除"原生事件,做组件 API 设计。

十一、tsconfig 关键配置 ​

json
{
  "compilerOptions": {
    "jsx": "react-jsx",             // ⚠️ 新 JSX 运行时,不用 import React
    "strict": true,                  // 严格模式
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true
  }
}

十二、本章小结 ​

要点关键
Propsinterface + 普通函数组件
继承原生属性extends HTMLAttributes<T>
useState显式标注联合/判别类型
useRefuseRef<T>(null) + ?.
事件类型React.XxxEvent<T>
泛型组件<T> 形参 + keyof 约束
不要React.FC(隐式 children 没了)

动手练习 ​

  1. Button 类型:支持 variant、size、disabled,继承原生 button 属性
  2. 泛型 List:接受任意类型数组 + render 函数
  3. AsyncState:用判别联合类型实现 loading/data/error 三态
  4. 找坑:以下代码 TS 报错为什么?
    tsx
    const ref = useRef<HTMLInputElement>(null);
    ref.current.focus();

下一章:第 20 章:Next.js 入门 →

本站基于 VitePress 构建 · 由 StackHub 团队维护