在前端开发的领域中,React 已经成为构建用户界面的一大主力框架。然而,随着项目规模的扩大和代码复杂性的提升,JavaScript 的动态类型特性常常让开发者在维护和扩展代码时面临挑战。正是在这种背景下,TypeScript(TS)作为 JavaScript 的强类型扩展语言,逐渐成为 React 开发的理想搭档。
对于刚开始使用 TypeScript 编写 React 的前端同学来说,TS 提供的静态类型检查和丰富的开发工具支持,能够显著提升开发体验。借助 TypeScript,开发者可以更早地在编译阶段捕获潜在的错误,避免由于类型不一致导致的运行时问题。此外,TypeScript 的类型系统为组件和函数提供了更明确的接口定义,这不仅有助于提高代码的可读性和可维护性,还能使团队协作变得更加顺畅。
尽管在刚接触时,TypeScript 可能会让人感觉到一定的学习曲线,尤其是在处理类型推断、泛型等概念时需要一些适应时间。但一旦掌握了这些基础,开发者会发现 TypeScript 能大幅减少调试时间,并让代码更加健壮、可预测。对于刚入门的同学来说,使用 TypeScript 编写 React 代码不仅是在学习一项技能,更是为未来的高效开发和项目的长期维护打下坚实的基础。
如下是针对个人理解整理的一些基础内容,希望能帮助大家更快的上手。
基础的Prop类型举例
type AppProps = {
message: string;
count: number;
disabled: boolean;
/** 一个类型的数组 */
names: string[];
/** 字符串字面量 */
status: "waiting" | "success";
/** 含有已知属性的对象,运行时可以添加更多的属性 */
obj: {
id: string;
title: string;
};
/** 对象数组(常用)*/
objArr: {
id: string;
title: string;
}[];
/** 非原始值,不能访问任何属性,不太常用,但是可以用来作为占位符 */
obj2: object;
// 一个空对象但是没有需要必填的属性,不太常用,有点像React.Component<{}, State>
obj3: {};
// 字典对象有种任意类型的key值,和同样类型的value值
dict1: {
[key: string]: MyTypeHere;
};
dict2: Record<string, MyTypeHere>; // 和dict1等价
// 没有任何参数和返回值的函数,很常用
onClick: () => void;
/** 有着具名参数的函数,没有返回值,很常用 */
onChange: (id: number) => void;
/** 参数是事件的函数,没有返回值,很常用 */
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/** 另一个参数是事件的函数,没有返回值,很常用 */
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
/** 声明任何你不打算去调用的函数,不推荐使用这个声明 */
onSomething: Function;
/** 可选属性,很常用 */
optional?: OptionalType;
/** hooks中useState的设置函数类型,number可以认为是泛型 */
setState: React.Dispatch<React.SetStateAction<number>>;
};
很有用的React Prop类型举例
export declare interface AppProps {
// 最常用的,泛指React可以render的任何内容
children?: React.ReactNode;
// 单个React.ReactElement
childrenElement: React.JSX.Element;
// 传递样式属性
style?: React.CSSProperties;
// 表单事件,指定的泛型参数是event.target的类型
onChange?: React.FormEventHandler<HTMLInputElement>;
}
/**
编辑器推断为:
const MyComponent: (props: {
foo: number;
bar: string;
}) => React.JSX.Element
*/
const MyComponent = (props: { foo: number, bar: string }) => <div />;
/**
编辑器推断为:
type MyComponentProps = {
foo: number;
bar: string;
}
*/
// 比较好用的一个工具类,直接从组件从提取props类型
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
// 这个地方既可以接受组件类型,也可以接受字符串,字符串一般是svg或者html元素的字面量
/**
编辑器推断为:
type ButtonComponentProps = React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement>
*/
type ButtonComponentProps = React.ComponentProps<'button'>;
// 看到上面有个ClassAttributes,我们再来分析一下它
interface ClassAttributes<T> extends RefAttributes<T> {
}
// 可以推测出ButtonComponentProps上会含有一个可有可无的ref属性
// 那么我们日常开发的过程中大概率是不会用到ref的,所以呢官方就推荐了另外两个工具类给我们
// ComponentPropsWithRef和ComponentPropsWithoutRef,官方建议我们显式地去声明一下
interface RefAttributes<T> extends Attributes {
ref?: LegacyRef<T> | undefined;
}
React.JSX.Element与React.ReactNode的区别
-
React.JSX.Element是React.createElement函数的返回值,返回的是一个对象。 -
React.ReactNode是组件的返回值。
可以看一下类型声明
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined
| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[
keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES
];
// 这个结构可以理解成虚拟dom
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>,
> {
type: T;
props: P;
key: string | null;
}
namespace JSX {
// JSX.Element是ReactElement的子集
interface Element extends React.ReactElement<any, any> {}
}
可以得出结论ReactNode包含了JSX.Element。
Type与Interface的选择
在定义公共 API 时使用 interface
- 在编写库或第三方环境类型定义时,建议使用
interface而不是type。 - 主要原因是
interface支持 声明合并(declaration merging),这意味着如果使用者需要在后续添加额外的属性或扩展接口,可以很方便地进行扩展。
// 存在于antd的三方包中
interface ButtonProps {
timeout: number;
}
// 之后,使用者可以在他们自己的代码中扩展它
// 举个例子,如果想对antd的ButtonProps进行拓展
declare module 'antd5' {
interface ButtonProps {
kk: number;
}
}
import { ButtonProps } from 'antd';
function TestButton(a: ButtonProps) {
const { kk } = a;
return <div>{kk}</div>;
}
在 React 组件的 Props 和 State 中使用 type:
type更适合在这里使用,因为它更具 约束性,并且更适合 React 组件的需求。React组件的props和state通常不需要支持声明合并,type在定义联合类型、映射和组合时更加灵活,因此在定义组件时使用type更合适。
type MyComponentProps = {
title: string;
isVisible: boolean;
};
const MyComponent: React.FC<MyComponentProps> = ({ title, isVisible }) => {
return <h1>{isVisible ? title : null}</h1>;
};
简单来说:
- 使用
interface保持 可扩展性和灵活性,适用于 公共 API。 - 使用
type保持 简单性和一致性,适用于 React 组件。
函数组件
// 声明属性的类型
type AppProps = {
message: string;
};
// 最简单的声明函数式组件的方式,返回类型让TS去推断
const App = ({ message }: AppProps) => <div>{message}</div>;
// 标注返回类型,这样如果返回值类型不匹配的话就会报错
const App = ({ message }: AppProps): React.JSX.Element => <div>{message}</div>;
// 内联的方式来声明,不过看起来有些多余,类型也没有办法被导出
const App = ({ message }: { message: string }) => <div>{message}</div>;
// 可以使用React.FunctionComponent和React.FC加泛型的方式来声明
const App: React.FunctionComponent<{ message: string }> = ({ message }) => (
<div>{message}</div>
);
// 或者
const App: React.FC<AppProps> = ({ message }) => <div>{message}</div>;
Hooks
useState
// 处理基础类型
const [state, setState] = useState(false);
// `state` 被推断为boolean类型
// `setState` 只能接受boolean类型的参数
// 处理引用类型
// 方法1
const [user, setUser] = useState<User | null>(null);
// 使用
setUser(newUser);
// 方法2
const [user, setUser] = useState<User>({} as User);
// 使用
setUser(newUser);
useCallback
const memoizedCallback = useCallback(
(param1: string, param2: number) => {
console.log(param1, param2)
return { ok: true }
},
[...],
);
/**
* 编辑器会展示如下的类型
* const memoizedCallback:
* (param1: string, param2: number) => { ok: boolean }
*/
useCallback的签名在React18版本前后有些不同
React<18
function useCallback<T extends (...args: any[]) => any>( callback: T, deps: DependencyList ): T;React>=18
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
useReducer
import { useReducer } from "react";
const initialState = { count: 0 };
type ACTIONTYPE =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: string };
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case "increment":
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - Number(action.payload) };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
-
</button>
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
+
</button>
</>
);
}
useEffect / useLayoutEffect
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(
() =>
setTimeout(() => {
/* 其他代码 */
}, timerMs),
[timerMs]
);
// 注意一下这个地方,setTimeout会返回一个数字,我们的箭头函数没有用大括号进行包裹,那么useEffect也会返回一个number类型,使用下面的方式更好一些
return null;
}
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(() => {
setTimeout(() => {
/* 其他代码 */
}, timerMs);
}, [timerMs]);
return null;
}
useRef
DOM
function Foo() {
// 如果可能的话,这个地方尽可能具体一下,`HTMLDivElement`比`HtmlElement`和`Element`更好。
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// 注意在你忘记赋值或者dom节点条件渲染的情况下,ref.current有可能是空的
if (!divRef.current) throw Error("divRef is not assigned");
// 这个时候的divRef.current就确定为HTMLDivElement了
doSomethingWith(divRef.current);
});
// 将ref赋值给元素,以便React可以帮你管理它
return <div ref={divRef}>etc</div>;
}
// 如果你确定divRef.current永远不为空的话,可以添加!
const divRef = useRef<HTMLDivElement>(null!);
// 后面操作的代码就可以不用判断是否为空了
doSomethingWith(divRef.current);
Mutable value ref
function Foo() {
// 编辑器推断的返回值为MutableRefObject<number | null>
const intervalRef = useRef<number | null>(null);
// 你也可以自己去管理它
useEffect(() => {
intervalRef.current = setInterval(...);
return () => clearInterval(intervalRef.current);
}, []);
// 这个ref没有被传递到htmlElement上
return <button onClick={/* clearInterval the ref */}>Cancel timer</button>;
}
useImperativeHandle
// Countdown.tsx
// 定义要被传入到forwardRef上的处理类型
export type CountdownHandle = {
start: () => void;
};
type CountdownProps = {};
const Countdown = forwardRef<CountdownHandle, CountdownProps>((props, ref) => {
useImperativeHandle(ref, () => ({
start() {
alert("Start");
},
}));
return <div>Countdown</div>;
});
// 使用Countdown的组件
import Countdown, { CountdownHandle } from "./Countdown.tsx";
function App() {
const countdownEl = useRef<CountdownHandle>(null);
useEffect(() => {
if (countdownEl.current) {
// 这个时候就可以推断出countdownEl的类型,并给出提示
countdownEl.current.start();
}
}, []);
return <Countdown ref={countdownEl} />;
}
自定义hooks
import { useState } from "react";
export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
/**
这个地方注意一下,如果as const会推断为元祖,否则会推断为任意类型的数组
有const 声明的推断
function useLoading(): readonly [any, (aPromise: Promise<any>) => Promise<any>]
没有as const的推断
function useLoading(): any[]
*/
return [isLoading, load] as const;
}
类组件
type MyProps = {
message: string;
};
type MyState = {
count: number;
};
class App extends React.Component<MyProps, MyState> {
state: MyState = {
count: 0,
};
render() {
return (
<div>
{this.props.message} {this.state.count}
</div>
);
}
}
表单与事件
// 如果将函数直接定义在jsx中,那么类型将会被自动处理
const el = (
<button
onClick={(event) => {
/* event will be correctly typed automatically! */
}}
/>
);
// 如果分开定义的话,我们就需要指定事件类型了
type State = {
text: string;
};
class App extends React.Component<Props, State> {
state = {
text: "",
};
// 类型声明在等号的右边
onChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ text: e.currentTarget.value });
};
/**
类型声明在等号的左边
onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({text: e.currentTarget.value})
}
*/
render() {
return (
<div>
<input type="text" value={this.state.text} onChange={this.onChange} />
</div>
);
}
}
如果不是特别关心事件类型的话,可以使用React.SyntheticEvent,举一个form的例子
<form
ref={formRef}
onSubmit={(e: React.SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
email: { value: string };
password: { value: string };
};
const email = target.email.value; // typechecks!
const password = target.password.value; // typechecks!
// etc...
}}
>
<div>
<label>
Email:
<input type="email" name="email" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Log in" />
</div>
</form>
常用事件列表
| Event Type | Description |
|---|---|
| ChangeEvent | input,select,textarea元素值的改变 |
| FocusEvent | 当元素获取或丢失节点的时候 |
| FormEvent | 当表单和表单元素获取或者失去焦点,表单元素值改变或者表单被提交时触发。 |
| KeyboardEvent | 用户与键盘的交互 |
| MouseEvent | 用户与鼠标的交互 |
| SyntheticEvent | 上面所有事件的基类,如果不确定用哪个的话可以用这个 |
// 放一个简单的事件使用的例子
"use client";
import {
useState,
ChangeEvent,
FocusEvent,
FormEvent,
MouseEvent,
KeyboardEvent,
} from "react";
export default function Home() {
const [name, setName] = useState("");
return (
<form
onSubmit={(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log("🚀 ~ Home ~ formData.get('name'):", formData.get('name'))
}}
>
<div>
<label htmlFor="name">姓名</label>
<input
name="name"
id="name"
value={name}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setName(e.target.value)
}
onFocus={(e: FocusEvent<HTMLInputElement>) =>
console.log(e.target.value, "focus")
}
onClick={(e: MouseEvent<HTMLInputElement>) =>
console.log(e.currentTarget, "click")
}
onKeyUp={(e: KeyboardEvent<HTMLInputElement>) =>
console.log(e.key, "keyup")
}
/>
</div>
<button type="submit">提交</button>
</form>
);
}
Context
import { createContext } from "react";
// 定义Context及其类型
type ThemeContextType = "light" | "dark";
const ThemeContext = createContext<ThemeContextType>("light");
import { useState } from "react";
// 定义Provider
const App = () => {
const [theme, setTheme] = useState<ThemeContextType>("light");
return (
<ThemeContext.Provider value={theme}>
<MyComponent />
</ThemeContext.Provider>
);
};
// 使用
import { useContext } from "react";
const MyComponent = () => {
const theme = useContext(ThemeContext);
return <p>The current theme is {theme}.</p>;
};
没有默认值的Context
import { createContext } from "react";
interface CurrentUserContextType {
username: string;
}
const CurrentUserContext = createContext<CurrentUserContextType | null>(null);
const App = () => {
const [currentUser, setCurrentUser] = useState<CurrentUserContextType>({
username: "filiptammergard",
});
return (
<CurrentUserContext.Provider value={currentUser}>
<MyComponent />
</CurrentUserContext.Provider>
);
};
import { useContext } from "react";
const MyComponent = () => {
const currentUser = useContext(CurrentUserContext);
return <p>Name: {currentUser?.username}.</p>;
};
另一种更好的方式是不去检查null类型,因为我们知道context不会是null,可以简单的进行异常抛出
import { createContext } from "react";
interface CurrentUserContextType {
username: string;
}
const CurrentUserContext = createContext<CurrentUserContextType | null>(null);
const useCurrentUser = () => {
const currentUserContext = useContext(CurrentUserContext);
if (!currentUserContext) {
throw new Error(
"useCurrentUser has to be used within <CurrentUserContext.Provider>"
);
}
return currentUserContext;
};
也可以使用如下断言的方式来处理
// 方法一
import { useContext } from "react";
const MyComponent = () => {
const currentUser = useContext(CurrentUserContext);
return <p>Name: {currentUser!.username}.</p>;
};
// 方法二
const CurrentUserContext = createContext<CurrentUserContextType>(
{} as CurrentUserContextType
);
// 方法三
const CurrentUserContext = createContext<CurrentUserContextType>(null!);
forwardRef/createRef
createRef
import { createRef, PureComponent } from "react";
class CssThemeProvider extends PureComponent<Props> {
private rootRef = createRef<HTMLDivElement>(); // like this
render() {
return <div ref={this.rootRef}>{this.props.children}</div>;
}
}
forwardRef
import { forwardRef, ReactNode } from "react";
interface Props {
children?: ReactNode;
type: "submit" | "button";
}
export type Ref = HTMLButtonElement;
export const FancyButton = forwardRef<Ref, Props>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));
泛型相关的forwardRefs(不常用)
将Ref放到属性里面
type ClickableListProps<T> = {
items: T[];
onSelect: (item: T) => void;
mRef?: React.Ref<HTMLUListElement> | null;
};
export function ClickableList<T>(props: ClickableListProps<T>) {
return (
<ul ref={props.mRef}>
{props.items.map((item, i) => (
<li key={i}>
<button onClick={(el) => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
重新声明forwardRef
// 重新声明 forwardRef
declare module "react" {
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}
import { forwardRef, ForwardedRef } from "react";
interface ClickableListProps<T> {
items: T[];
onSelect: (item: T) => void;
}
function ClickableListInner<T>(
props: ClickableListProps<T>,
ref: ForwardedRef<HTMLUListElement>
) {
return (
<ul ref={ref}>
{props.items.map((item, i) => (
<li key={i}>
<button onClick={(el) => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
export const ClickableList = forwardRef(ClickableListInner);
更改调用签名
// 添加到 `index.d.ts`
interface ForwardRefWithGenerics extends React.FC<WithForwardRefProps<Option>> {
<T extends Option>(props: WithForwardRefProps<T>): ReturnType<
React.FC<WithForwardRefProps<T>>
>;
}
export const ClickableListWithForwardRef: ForwardRefWithGenerics =
forwardRef(ClickableList);
Portals
// 假定在跟root同级的地方有一个#modal-root元素
const modalRoot = document.getElementById("modal-root") as HTMLElement;
export class Modal extends React.Component<{ children?: React.ReactNode }> {
el: HTMLElement = document.createElement("div");
componentDidMount() {
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(this.props.children, this.el);
}
}
// hooks版本
import { useEffect, useRef, ReactNode } from "react";
import { createPortal } from "react-dom";
const modalRoot = document.querySelector("#modal-root") as HTMLElement;
type ModalProps = {
children: ReactNode;
};
function Modal({ children }: ModalProps) {
const elRef = useRef<HTMLDivElement | null>(null);
if (!elRef.current) elRef.current = document.createElement("div");
useEffect(() => {
const el = elRef.current!; // 非空断言,因为这个地方不会是空的
modalRoot.appendChild(el);
return () => {
modalRoot.removeChild(el);
};
}, []);
return createPortal(children, elRef.current);
}
使用示例
import { useState } from "react";
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
// you can also put this in your static html file
<div id="modal-root"></div>
// 这个地方要注意一下事件冒泡
{showModal && (
<Modal>
<div
style={{
display: "grid",
placeItems: "center",
height: "100vh",
width: "100vh",
background: "rgba(0,0,0,0.1)",
zIndex: 99,
}}
>
I'm a modal!{" "}
<button
style={{ background: "papyawhip" }}
onClick={() => setShowModal(false)}
>
close
</button>
</div>
</Modal>
)}
<button onClick={() => setShowModal(true)}>show Modal</button>
// rest of your app
</div>
);
}
Error Boundaries
React-error-boundary
内置TS支持的轻量级的包。
自定义
import React, { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children?: ReactNode;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(_: Error): State {
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}
public render() {
if (this.state.hasError) {
return <h1>Sorry.. there was an error</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
通过这些例子,我们可以看到,TypeScript 在 React 开发中的优势不止于提供类型检查,它更像是一位智能的助手,帮助我们写出更安全、易维护的代码。虽然对于刚入门的前端开发者来说,TS 的学习过程可能会有些复杂和不适应,但它所带来的长远收益是显而易见的——从减少运行时错误,到提升团队协作效率,再到为代码提供清晰的结构和文档。
作为刚开始使用 TypeScript 编写 React 的开发者,最重要的是保持耐心,逐步适应类型系统,并从小项目开始积累经验。相信随着时间的推移,TypeScript 会成为你开发工具箱中不可或缺的一部分,让你在编写 React 应用时更加自信和高效。拥抱这个学习曲线,不仅能帮助你编写出更强健的代码,还能让你在日后的开发中走得更远更顺畅。