Skip to content
第 156 / 250 章前端⏱ 10 分钟阅读

第 156 章:TypeScript 集成

学习目标

  • 掌握 React 组件的类型化
  • 学会泛型组件
  • 理解事件类型
  • 解决常见类型问题

一、组件 Props 类型

1.1 interface

tsx
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

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

1.2 继承原生属性

tsx
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

function Input({ label, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
    </label>
  );
}

二、Children 类型

tsx
interface CardProps {
  children: React.ReactNode;          // 任意可渲染
  // 或:
  // children: React.ReactElement;   // 单个元素
  // children: (data: T) => ReactNode;  // 函数
}

三、事件类型

tsx
import {
  ChangeEvent,
  FormEvent,
  MouseEvent,
  KeyboardEvent,
  FocusEvent,
} from 'react';

function Form() {
  function onChange(e: ChangeEvent<HTMLInputElement>) {
    console.log(e.target.value);
  }
  function onClick(e: MouseEvent<HTMLButtonElement>) {
    console.log(e.currentTarget.disabled);
  }
  function onSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
  }
  function onKey(e: KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') { /* ... */ }
  }
  function onFocus(e: FocusEvent<HTMLInputElement>) { /* ... */ }

  return <form onSubmit={onSubmit}><input onChange={onChange} onKeyDown={onKey} /><button onClick={onClick} /></form>;
}

四、useState 类型

4.1 自动推断

tsx
const [count, setCount] = useState(0);         // number
const [name, setName] = useState('Tom');       // string
const [list, setList] = useState([1, 2, 3]);   // number[]

4.2 显式标注

tsx
const [user, setUser] = useState<User | null>(null);
const [list, setList] = useState<User[]>([]);

4.3 函数式更新

tsx
setList(prev => [...prev, newItem]);   // prev 类型自动推断

五、useRef 类型

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

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

六、useReducer 类型

tsx
interface State { count: number; }
type Action = { type: 'inc' } | { type: 'dec' } | { type: 'set'; payload: 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.payload };
    default: return state;
  }
}

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

七、Context 类型

tsx
import { createContext, useContext } from 'react';

interface Theme {
  mode: 'light' | 'dark';
  toggle: () => void;
}

const ThemeContext = createContext<Theme | null>(null);

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('必须在 Provider 内');
  return ctx;
}

八、泛型组件

8.1 泛型列表

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

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

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

8.2 泛型 Hook

tsx
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(initial);
  // ...
  return [value, setValue] as const;
}

// 使用(自动推断)
const [count, setCount] = useLocalStorage('count', 0);
const [user, setUser] = useLocalStorage<User | null>('user', null);

九、forwardRef 类型(React 19 前)

tsx
import { forwardRef, Ref } from 'react';

interface InputProps {
  label: string;
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label }, ref) => (
    <>
      <label>{label}</label>
      <input ref={ref} />
    </>
  )
);

// 使用
const ref = useRef<HTMLInputElement>(null);
<Input label="姓名" ref={ref} />

React 19:ref 直接作为 prop,不再需要 forwardRef

十、常见类型问题

10.1 event.currentTarget 可能为 null

tsx
function handler(e: MouseEvent<HTMLButtonElement>) {
  // ✅ 显式判断
  if (e.currentTarget) {
    e.currentTarget.disabled = true;
  }
}

10.2 数组 map 中的 key

tsx
// ❌ key 类型问题
{items.map((item: any) => <li key={item.id}>...</li>)}

// ✅ 类型化
interface Item { id: number; name: string; }
{items.map((item: Item) => <li key={item.id}>...</li>)}

10.3 第三方库类型缺失

bash
pnpm add @types/xxx
# 或
pnpm add -D @types/react @types/react-dom

十一、组件库类型示例

tsx
// 引入第三方库类型
import { Button, Modal } from 'antd';

// 自定义包装
type AntButtonProps = React.ComponentProps<typeof Button> & {
  customProp?: string;
};

function MyButton(props: AntButtonProps) {
  return <Button {...props} />;
}

十二、本章小结

概念关键
Propsinterface 或 type
事件ChangeEvent<HTMLInputElement>
useState推断或显式标注
useRef<HTMLInputElement | null>
泛型组件<T> 接收任意类型
常见坑currentTarget null、key 类型

动手练习

  1. 给现有组件加 TypeScript 类型
  2. 写一个泛型 Select<T> 组件
  3. 写一个泛型 useApi<T> Hook
  4. 修复一个未类型化的项目

推荐阅读


下一章:第 157 章:Tailwind CSS 样式

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