TypeScript × React 实战
概述
React 与 TypeScript 的结合是现代前端开发的标准实践。TypeScript 为 React 组件提供了 props 类型检查、事件类型推断、ref 类型安全等能力。但 React 的某些模式(泛型组件、forwardRef、children)在类型标注上有独特的挑战。
面试定位:TypeScript × React 的类型问题是前端面试的高频考点。面试官期望候选人能流畅地标注组件 props、事件处理器和 ref,并了解
React.FC的利弊。
组件 Props 类型
基础标注
// Interface 定义 Props
interface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
onClick: () => void;
}
function Button({ label, variant = "primary", disabled, onClick }: ButtonProps) {
return (
<button className={variant} disabled={disabled} onClick={onClick}>
{label}
</button>
);
}
React.FC 的利弊
// React.FC 写法
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
React.FC 的问题(React 18 之前):
- 曾经隐式包含
childrenprop,即使组件不接受 children - React 18 已移除此行为
当前推荐:直接标注参数类型,不使用 React.FC:
// 推荐写法
function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
Children 类型
// 接受任何有效的 React 子元素
interface LayoutProps {
children: React.ReactNode;
}
// 只接受单个 React 元素
interface WrapperProps {
children: React.ReactElement;
}
// 渲染函数模式
interface DataListProps<T> {
items: T[];
children: (item: T, index: number) => React.ReactNode;
}
事件类型
常用事件类型
function Form() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") { /* ... */ }
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} />
<button onClick={handleClick}>Submit</button>
</form>
);
}
事件处理器类型 vs 事件类型
// 事件对象类型
type ClickEvent = React.MouseEvent<HTMLButtonElement>;
// 事件处理器类型(包含事件参数的函数签名)
type ClickHandler = React.MouseEventHandler<HTMLButtonElement>;
// 两种写法等价
const handler1: ClickHandler = (e) => { /* ... */ };
const handler2 = (e: ClickEvent) => { /* ... */ };
泛型组件
基础泛型组件
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// 使用:T 自动推断
<List
items={[{ id: "1", name: "Alice" }]}
renderItem={(user) => <span>{user.name}</span>} // user 类型自动推断
keyExtractor={(user) => user.id}
/>
泛型 Select 组件
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
}
function Select<T>({
options,
value,
onChange,
getLabel,
getValue,
}: SelectProps<T>) {
return (
<select
value={getValue(value)}
onChange={(e) => {
const selected = options.find(
(opt) => getValue(opt) === e.target.value
);
if (selected) onChange(selected);
}}
>
{options.map((opt) => (
<option key={getValue(opt)} value={getValue(opt)}>
{getLabel(opt)}
</option>
))}
</select>
);
}
泛型组件与 forwardRef 的结合
泛型组件无法直接与 forwardRef 结合(forwardRef 不保留泛型参数)。常见解决方案:
// 方案一:类型断言
const List = forwardRef(<T,>(
props: ListProps<T>,
ref: React.ForwardedRef<HTMLUListElement>
) => {
return <ul ref={ref}>{/* ... */}</ul>;
}) as <T>(props: ListProps<T> & { ref?: React.Ref<HTMLUListElement> }) => React.ReactElement;
// 方案二:使用 React 19 的 ref prop(推荐)
// React 19 中 ref 是普通 prop,不需要 forwardRef
function List<T>({ items, ref }: ListProps<T> & { ref?: React.Ref<HTMLUListElement> }) {
return <ul ref={ref}>{/* ... */}</ul>;
}
forwardRef 类型
基础用法
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...rest }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} {...rest} />
{error && <span className="error">{error}</span>}
</div>
);
}
);
Input.displayName = "Input";
useImperativeHandle
interface ModalHandle {
open: () => void;
close: () => void;
}
interface ModalProps {
title: string;
children: React.ReactNode;
}
const Modal = forwardRef<ModalHandle, ModalProps>(({ title, children }, ref) => {
const [isOpen, setIsOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
}));
if (!isOpen) return null;
return (
<div className="modal">
<h2>{title}</h2>
{children}
</div>
);
});
// 使用
const modalRef = useRef<ModalHandle>(null);
modalRef.current?.open();
ComponentProps 与类型提取
从组件提取 Props 类型
import { ComponentProps, ComponentPropsWithRef } from "react";
// 从 HTML 元素提取
type ButtonNativeProps = ComponentProps<"button">;
type InputNativeProps = ComponentProps<"input">;
// 从自定义组件提取
type MyButtonProps = ComponentProps<typeof Button>;
// 包含 ref 的版本
type InputWithRef = ComponentPropsWithRef<"input">;
扩展 HTML 元素 Props
interface CustomButtonProps extends ComponentProps<"button"> {
variant: "primary" | "secondary";
isLoading?: boolean;
}
function CustomButton({ variant, isLoading, children, ...rest }: CustomButtonProps) {
return (
<button className={variant} disabled={isLoading} {...rest}>
{isLoading ? "Loading..." : children}
</button>
);
}
多态组件(Polymorphic Component)
type PolymorphicProps<E extends React.ElementType, P = {}> = P &
Omit<ComponentProps<E>, keyof P | "as"> & {
as?: E;
};
function Box<E extends React.ElementType = "div">({
as,
...props
}: PolymorphicProps<E>) {
const Component = as || "div";
return <Component {...props} />;
}
// 使用
<Box as="a" href="/home">Link</Box> // href 可用
<Box as="button" onClick={() => {}}>Button</Box> // onClick 可用
Hooks 类型
useState
// 自动推断
const [count, setCount] = useState(0); // number
// 显式泛型(初始值为 null 时需要)
const [user, setUser] = useState<User | null>(null);
useRef
// DOM ref
const inputRef = useRef<HTMLInputElement>(null);
// 可变 ref
const timerRef = useRef<number | undefined>(undefined);
timerRef.current = window.setTimeout(() => {}, 1000);
useReducer
type State = { count: number; step: number };
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "setStep"; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { ...state, count: state.count + state.step };
case "decrement": return { ...state, count: state.count - state.step };
case "setStep": return { ...state, step: action.payload };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
dispatch({ type: "setStep", payload: 5 }); // 类型安全
面试高频问题
Q: React.FC 还值得使用吗?
回答要点:React 18 之后,React.FC 已移除了隐式 children 的问题。但它仍然有局限性——无法用于泛型组件,且多了一层类型包装。当前 React 社区(包括 React 官方文档)倾向于直接在函数参数上标注 Props 类型,这种方式更灵活,也能更好地支持泛型。
Q: 如何为泛型组件添加 forwardRef?
回答要点:传统的 forwardRef 不保留泛型参数,需要类型断言。React 19 的解决方案是将 ref 变为普通 prop,组件可以直接从 props 接收 ref,无需 forwardRef 包装,泛型自然保留。在 React 18 中,常用方案是将 forwardRef 的返回值通过类型断言恢复泛型签名。
Q: 如何正确标注事件处理函数?
回答要点:React 提供了泛型事件类型如 React.ChangeEvent<HTMLInputElement>。最简单的方式是先内联事件处理器让 TypeScript 自动推断,然后再提取为独立函数。也可以使用事件处理器类型 React.MouseEventHandler<HTMLButtonElement> 直接标注函数。