跳到主要内容

TypeScript 与 React

问题

如何在 React 中使用 TypeScript?组件 Props、Hooks、事件处理等如何定义类型?

答案

TypeScript 与 React 结合可以提供完整的类型安全,减少运行时错误。本文介绍组件类型定义、常用 Hooks 类型、事件处理、高阶组件等最佳实践。


函数组件类型

基础定义

// 方式一:直接定义 Props 类型
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}

function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}

// 方式二:使用 React.FC(不推荐)
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled }) => {
return <button onClick={onClick} disabled={disabled}>{label}</button>;
};
为什么不推荐 React.FC
  • 隐式定义 children(React 18 已移除)
  • 不支持泛型组件
  • 与 defaultProps 配合有问题 推荐直接使用函数声明或箭头函数。

children 类型

interface CardProps {
title: string;
children: React.ReactNode; // 最常用
}

function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}

// children 类型选择
interface Props {
// ReactNode:最宽泛,接受任何可渲染内容
children: React.ReactNode;

// ReactElement:只接受 JSX 元素
element: React.ReactElement;

// 字符串
text: string;

// Render Props
render: (data: User) => React.ReactNode;

// 函数作为子元素
children: (data: User) => React.ReactElement;
}

泛型组件

interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}

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

// 使用
interface User {
id: string;
name: string;
}

<List<User>
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>;

Hooks 类型

useState

// 自动推断
const [count, setCount] = useState(0); // number

// 显式指定(推荐用于可能为 null 的情况)
const [user, setUser] = useState<User | null>(null);

// 复杂类型
interface FormData {
name: string;
email: string;
age: number;
}

const [form, setForm] = useState<FormData>({
name: '',
email: '',
age: 0
});

// 函数式更新
setForm((prev) => ({ ...prev, name: 'Alice' }));

useRef

// DOM 引用
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
inputRef.current?.focus();
}, []);

// 可变值(不触发重渲染)
const countRef = useRef<number>(0);
countRef.current = 1; // 不需要 setter

// 区分:null vs undefined
const ref1 = useRef<HTMLDivElement>(null); // 只读,用于 DOM
const ref2 = useRef<number | null>(null); // 可变
const ref3 = useRef<number>(0); // 可变,有初始值

useReducer

interface State {
count: number;
error: string | null;
}

type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; payload: number }
| { type: 'error'; message: string };

function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'set':
return { ...state, count: action.payload };
case 'error':
return { ...state, error: action.message };
default:
return state;
}
}

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

return (
<div>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'set', payload: 10 })}>
Set 10
</button>
</div>
);
}

useContext

interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}

// 创建 Context
const ThemeContext = createContext<ThemeContextType | null>(null);

// 自定义 Hook(推荐)
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}

// Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');

const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
}, []);

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}

useCallback 和 useMemo

// useCallback 返回类型会自动推断
const handleClick = useCallback((id: number) => {
console.log(id);
}, []);
// 类型:(id: number) => void

// useMemo
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// 类型根据返回值推断

// 显式指定泛型
const formatted = useMemo<string>(() => {
return JSON.stringify(data);
}, [data]);

事件处理

常用事件类型

// 点击事件
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
};

// 表单提交
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
};

// 输入变化
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};

// 键盘事件
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
// ...
}
};

// 焦点事件
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
};

// 拖拽事件
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
const files = event.dataTransfer.files;
};

事件处理函数类型

// 作为 Props
interface FormProps {
onSubmit: (data: FormData) => void;
onChange: React.ChangeEventHandler<HTMLInputElement>;
onClick: React.MouseEventHandler<HTMLButtonElement>;
}

// 内联定义
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}}
/>;

// 省略类型(自动推断)
<button onClick={(e) => handleClick(e)}>Click</button>

组件 Props 模式

扩展 HTML 属性

// 方式一:ComponentPropsWithoutRef
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
variant?: 'primary' | 'secondary';
}

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

// 方式二:HTMLAttributes
interface InputProps extends React.HTMLAttributes<HTMLInputElement> {
label: string;
}

// 方式三:针对 input 特殊处理
type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> & {
size?: 'small' | 'medium' | 'large';
};

forwardRef 类型

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

const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
}
);

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

条件 Props

// 使用联合类型
type ButtonProps =
| {
variant: 'link';
href: string;
// onClick 不存在
}
| {
variant: 'button';
onClick: () => void;
// href 不存在
};

function Button(props: ButtonProps) {
if (props.variant === 'link') {
return <a href={props.href}>Link</a>;
}
return <button onClick={props.onClick}>Button</button>;
}

// 使用
<Button variant="link" href="/about" />;
<Button variant="button" onClick={() => {}} />;

多态组件(as prop)

type AsProp<C extends React.ElementType> = {
as?: C;
};

type PolymorphicProps<C extends React.ElementType, Props = {}> =
AsProp<C> &
Omit<React.ComponentPropsWithoutRef<C>, keyof AsProp<C>> &
Props;

interface TextOwnProps {
size?: 'small' | 'medium' | 'large';
}

type TextProps<C extends React.ElementType = 'span'> = PolymorphicProps<
C,
TextOwnProps
>;

function Text<C extends React.ElementType = 'span'>({
as,
size = 'medium',
...props
}: TextProps<C>) {
const Component = as || 'span';
return <Component className={`text-${size}`} {...props} />;
}

// 使用
<Text>Default span</Text>;
<Text as="h1" size="large">Heading</Text>;
<Text as="a" href="/about">Link</Text>;

高阶组件(HOC)

interface WithLoadingProps {
loading: boolean;
}

function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function WithLoadingComponent({
loading,
...props
}: P & WithLoadingProps) {
if (loading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...(props as P)} />;
};
}

// 使用
interface UserProps {
user: User;
}

const UserCard = ({ user }: UserProps) => <div>{user.name}</div>;
const UserCardWithLoading = withLoading(UserCard);

<UserCardWithLoading user={user} loading={false} />;

常见面试问题

Q1: React.FC 的问题是什么?

答案

// 1. 隐式 children(React 18 已移除)
const Comp: React.FC<Props> = ({ children }) => {
// 之前:children 自动可用
// React 18:需要显式定义
};

// 2. 不支持泛型
// ❌ 不能这样写
const List: React.FC<ListProps<T>> = ...

// ✅ 直接定义
function List<T>(props: ListProps<T>) { ... }

// 3. defaultProps 支持不好
// ❌ 类型推断有问题
Comp.defaultProps = { };

// ✅ 使用参数默认值
function Comp({ value = 'default' }: Props) { ... }

// 推荐写法:直接定义函数,不用 React.FC
interface Props {
title: string;
children?: React.ReactNode;
}

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

Q2: 如何处理 useRef 的类型?

答案

// 三种场景:

// 1. DOM 引用(只读)
const divRef = useRef<HTMLDivElement>(null);
// divRef.current 是 HTMLDivElement | null
// 传递给 JSX:<div ref={divRef}>

// 2. 可变值容器
const countRef = useRef<number>(0);
// countRef.current 是 number
// 可以直接修改:countRef.current = 1

// 3. 可能为 null 的可变值
const timerRef = useRef<NodeJS.Timeout | null>(null);
timerRef.current = setTimeout(() => {}, 1000);
clearTimeout(timerRef.current!);

// 区分关键:
// useRef<T>(null) 且 T 不含 null → 只读,用于 DOM
// useRef<T | null>(null) → 可变
// useRef<T>(initialValue) → 可变

Q3: 如何定义泛型组件?

答案

// 方式一:函数声明
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
}

function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
return (
<select
value={String(value)}
onChange={(e) => {
const selected = options.find(
(o) => String(o) === e.target.value
);
if (selected) onChange(selected);
}}
>
{options.map((option) => (
<option key={String(option)} value={String(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}

// 使用
<Select<User>
options={users}
value={selectedUser}
onChange={setSelectedUser}
getLabel={(u) => u.name}
/>;

// 方式二:箭头函数
const Select = <T,>(props: SelectProps<T>) => {
// 注意:<T,> 需要逗号,否则 JSX 解析错误
};

Q4: 如何正确处理事件类型?

答案

// 内联:类型自动推断
<input onChange={(e) => setValue(e.target.value)} />;

// 单独定义:需要指定类型
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setValue(e.target.value);
};

// 或
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};

// 常用事件类型:
// React.MouseEvent<HTMLButtonElement>
// React.ChangeEvent<HTMLInputElement>
// React.FormEvent<HTMLFormElement>
// React.KeyboardEvent<HTMLInputElement>
// React.FocusEvent<HTMLInputElement>
// React.DragEvent<HTMLDivElement>

// 泛型元素
// React.SyntheticEvent<HTMLElement>

Q5: 如何定义可扩展的组件 Props?

答案

// 方式一:继承 HTML 属性
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
}

// 方式二:Omit 冲突属性
interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
size?: 'small' | 'medium' | 'large'; // 自定义 size
}

// 方式三:ComponentPropsWithoutRef
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {
variant?: 'primary' | 'secondary';
};

// 方式四:ComponentPropsWithRef(需要转发 ref)
type InputProps = React.ComponentPropsWithRef<'input'> & {
label: string;
};

// 使用
function Button({ variant = 'primary', ...props }: ButtonProps) {
return <button className={variant} {...props} />;
}

// 外部可以传递任何 button 属性
<Button
variant="primary"
onClick={() => {}}
disabled
aria-label="Submit"
/>;

相关链接