机器翻译正文由机器翻译自项目原始文档(英文),排版经程序统一处理,可能存在偏差,请以原项目仓库为准。
React TypeScript 备忘单
使用 TypeScript 的 React 备忘单。
:wave: 这个仓库由 @eps1lon 和 @filiptammergard 维护。我们很高兴你想尝试使用 TypeScript 的 React!如果你发现任何错误或遗漏,请 提交一个问题! : 1:
|
备忘单 旨在帮助 React 开发者高效使用 TypeScript:
- 提供有主见的最佳实践和可直接复制粘贴的示例。
- 涵盖基础的 TS 类型和设置,以及针对编写可重用类型工具和 React TS 库 的高级泛型类型使用。
- 提供参与 DefinitelyTyped 的建议。
备忘单
简介
先决条件
你可以在任何技能水平下使用这张备忘单作为参考,但假设你对 React 和 TypeScript 有基本的了解。以下是先决条件列表:
- 对 React 有基本了解。
- 熟悉 TypeScript 基础 和 日常类型。
在这张备忘单中,我们假设你使用的是最新版的 React 和 TypeScript。
React 和 TypeScript 入门工具包
React 提供了 如何启动新 React 项目 的文档,支持一些最流行的框架。以下是使用 TypeScript 启动它们的方法:
- Next.js:
npx create-next-app@latest --ts - Remix:
npx create-remix@latest - Gatsby:
npm init gatsby --ts - Expo:
npx create-expo-app -t with-typescript
如果你只想要一个无框架的客户端单页应用,Vite 是最常见的选择:
- Vite:
npm create vite@latest my-app -- --template react-ts
在线尝试 React 和 TypeScript
有一些工具可以让你在线运行 React 和 TypeScript,这对调试或创建可共享的复现非常有帮助。
入门
功能组件
这些可以写成普通函数,接收一个 props 参数并返回一个 JSX 元素。
// Declaring type of props - see "Typing Component Props" for more examples
type AppProps = {
message: string;
}; /* use `interface` if exporting so that consumers can extend */
// Easiest way to declare a Function Component; return type is inferred.
const App = ({ message }: AppProps) => <div>{message}</div>;
// You can choose to annotate the return type so an error is raised if you accidentally return some other type
const App = ({ message }: AppProps): React.JSX.Element => <div>{message}</div>;
// You can also inline the type declaration; eliminates naming the prop types, but looks repetitive
const App = ({ message }: { message: string }) => <div>{message}</div>;
// Alternatively, you can use `React.FunctionComponent` (or `React.FC`), if you prefer.
// With latest React types and TypeScript 5.1. it's mostly a stylistic choice, otherwise discouraged.
const App: React.FunctionComponent<{ message: string }> = ({ message }) => (
<div>{message}</div>
);
// or
const App: React.FC<AppProps> = ({ message }) => <div>{message}</div>;
提示:你可以使用 Paul Shen 的 VS Code 插件 来自动生成类型解构声明(包括 键盘快捷键)。
为什么 React.FC 不需要?那 React.FunctionComponent / React.VoidFunctionComponent 呢?
你可能会在很多 React TypeScript 代码库中看到这个:
const App: React.FunctionComponent<{ message: string }> = ({ message }) => (
< 除以 > {message} < /div >
);
然而,如今的普遍共识是 React.FunctionComponent(或简写 React.FC)并非必须。如果你仍在使用 React 17 或低于 5.1 的 TypeScript,甚至 不推荐使用。这当然是一个细微的观点,但如果你同意并希望从代码库中移除 React.FC,你可以使用 这个 jscodeshift codemod。
与 " 普通函数 " 版本的一些区别:
-
React.FunctionComponent对返回类型是显式的,而普通函数版本则是隐式的(否则需要额外的注解)。 -
它为静态属性提供类型检查和自动完成功能,如
displayName、propTypes和defaultProps。- 请注意,在
React.FunctionComponent中使用defaultProps时存在一些已知问题。详情请参见 此问题。我们还维护了一个单独的defaultProps部分,你也可以查阅。
- 请注意,在
-
将来,它可能会自动将 props 标记为
readonly,不过如果在参数列表中对 props 对象进行了结构分解,这一点就无关紧要了。
在大多数情况下,使用哪种语法几乎没有太大区别,但你可能更喜欢 React.FunctionComponent 更明确的特性。
钩子
从 v16.8 开始,@types/react 支持 Hooks。
useState
类型推断对于简单值非常有效:
const [state, setState] = useState(false);
// `state` is inferred to be a boolean
// `setState` only takes booleans
如果你需要使用一个你依赖推断的复杂类型,你可以使用 typeof 来获取推断的类型。
然而,许多钩子初始化时使用的是 null 类默认值,你可能会想如何提供类型。明确声明类型,并使用联合类型:
const [user, setUser] = useState<User | null>(null);
// later...
setUser(newUser);
如果状态在设置后不久被初始化,并且之后总是有一个值,你也可以使用类型断言:
const [user, setUser] = useState<User>({} as User);
// later...
setUser(newUser);
这会暂时“欺骗” TypeScript 编译器,让它认为 {} 的类型是 User。你应该接着设置 user 状态——如果不这样做,你代码的其他部分可能会依赖 user 是 User 类型,这可能导致运行时错误。
useCallback
你可以像对待其他函数一样来输入 useCallback。
const memoizedCallback = useCallback(
(param1: string, param2: number) => {
console.log(param1, param2)
return { ok: true }
},
[...],
);
/**
* VSCode will show the following type:
* const memoizedCallback:
* (param1: string, param2: number) => { ok: boolean }
*/
请注意,对于 React < 18,useCallback 的函数签名默认将类型化参数为 any[]:
function useCallback<T extends (...args: any[]) => any>(
callback: T,
deps: DependencyList,
): T;
在 React 18 及更高版本中,useCallback 的函数签名更改为以下形式:
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
因此,以下代码将在 React >= 18 中产生 "Parameter 'e' implicitly has an 'any' type." 错误,但在 React 17 中不会产生 "<"。
// @ts-expect-error Parameter 'e' implicitly has 'any' type.
useCallback((e) => {}, []);
// Explicit 'any' type.
useCallback((e: any) => {}, []);
useReducer
你可以将 判别联合 用于 reducer 的动作。不要忘记定义 reducer 的返回类型,否则 TypeScript 会自己推断。
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,
): typeof initialState {
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>
</>
);
}
与 redux 的 Reducer 一起使用
如果你使用 redux 库来编写 reducer 函数,它提供了一个方便的格式化辅助工具 Reducer<State, Action>,它会为你处理返回类型。
所以上面的 reducer 示例变为:
从 'redux' 导入 { Reducer };
导出函数 reducer: Reducer < AppState, Action > () {}
为 useReducer 提供显式类型
在大多数情况下,useReducer 的类型推断应该能可靠地工作。当推断失败时,可以使用以下语法显式提供状态和动作类型,其中动作类型被包裹在一个单元素元组中。
const [state, dispatch] = useReducer<typeof initialState, [ACTIONTYPE]>(
reducer,初始状态,
);
useEffect / useLayoutEffect
useEffect 和 useLayoutEffect 都用于执行 副作用,并返回一个可选的清理函数,这意味着如果它们不处理返回值,就不需要类型。在使用 useEffect 时,要注意不要返回除函数或 undefined 以外的任何值,否则 TypeScript 和 React 都会报错。在使用箭头函数时,这一点可能比较微妙:
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs],
);
// bad example! setTimeout implicitly returns a number
// because the arrow function body isn't wrapped in curly braces
return null;
}
上述示例的解决方案
函数 DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(() => {
setTimeout(() => {
/* do stuff */
}, timerMs);
}, [timerMs]);
// 更好;使用 void 关键字确保返回 undefined return null;
}
useRef
useRef 在当前的 @types/react 中总是返回一个 RefObject<T>。需要一个初始值,并且返回的 .current 是基于该值类型化的。(MutableRefObject 已被弃用,仅为向后兼容而保留。)
选项 1:DOM 元素引用
要访问 DOM 元素: 将元素类型作为泛型提供,并将 null 作为初始值。React 会为你管理 .current,TypeScript 期望你将此 ref 传递给元素的 ref 属性:
function Foo() {
// - If possible, prefer as specific as possible. For example, HTMLDivElement
// is better than HTMLElement and way better than Element.
// - Technical-wise, this returns RefObject<HTMLDivElement>
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Note that ref.current may be null. This is expected, because you may
// conditionally render the ref-ed element, or you may forget to assign it
if (!divRef.current) throw Error("divRef is not assigned");
// Now divRef.current is sure to be HTMLDivElement
doSomethingWith(divRef.current);
});
// Give the ref to an element so React can manage it for you
return <div ref={divRef}>etc</div>;
}
如果你确定 divRef.current 永远不会为 null,也可以使用非空断言操作符 !:
const divRef = useRef<HTMLDivElement>(null!);
// Later... No need to check if it is null
doSomethingWith(divRef.current);
请注意,您在这里选择放弃类型安全——如果在渲染时忘记将 ref 分配给某个元素,或者 ref 指向的元素是有条件渲染的,您将会遇到运行时错误。
提示:选择使用哪个 HTMLElement
Refs 需要具体类型 —— 仅仅指定任意 HTMLElement 是不够的。如果你不知道所需元素类型的名称,可以查看 lib.dom.ts,或者故意制造一个类型错误,让语言服务告诉你:
选项 2:可变值引用
在渲染过程中保持可变值而不在更改时重新渲染:传递你想要的初始值 — React 并不会为你管理 .current,你需要手动写入它。
function Foo() {
const intervalRef = useRef<number | null>(null);
useEffect(() => {
intervalRef.current = window.setInterval(() => {
/* ... */
}, 1000);
return () => {
if (intervalRef.current !== null) clearInterval(intervalRef.current);
};
}, []);
return (
<button
onClick={() => {
/* clearInterval the ref */
}}
>
Cancel timer
</button>
);
}
另请参阅
useImperativeHandle
在 React 19 中,ref 是函数组件的普通属性,所以 useImperativeHandle 直接用 ref 属性调用 —— 不需要 forwardRef。
// Countdown.tsx
import { useImperativeHandle, Ref } from "react";
export type CountdownHandle = {
start: () => void;
};
type CountdownProps = {
ref?: Ref<CountdownHandle>;
};
const Countdown = ({ ref }: CountdownProps) => {
useImperativeHandle(ref, () => ({
// start() has type inference here
start() {
alert("Start");
},
}));
return <div>Countdown</div>;
};
// The component using the Countdown component
import { useEffect, useRef } from "react";
import Countdown, { CountdownHandle } from "./Countdown.tsx";
function App() {
const countdownEl = useRef<CountdownHandle>(null);
useEffect(() => {
if (countdownEl.current) {
// start() has type inference here as well
countdownEl.current.start();
}
}, []);
return <Countdown ref={countdownEl} />;
}
如果你仍然维护针对 React < 19 的代码,请参阅forwardRef 部分了解使用
forwardRef<CountdownHandle, CountdownProps>的传统方法。
自定义钩子
如果你在自定义 Hook 中返回一个数组,你会想要避免类型推断,因为 TypeScript 会推断为联合类型(而实际上你希望数组中每个位置是不同的类型)。相反,可以使用 TS 3.4 const 断言 :
import { useState } from "react";
export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // infers [boolean, typeof load] instead of (boolean | typeof load)[]
}
这样,当你进行解构时,你实际上会根据解构位置得到正确的类型。
替代方案:断言元组返回类型
如果你在使用 const 断言时遇到问题,你也可以断言或定义函数的返回类型:
从 "react" 导入 { useState };
export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise < any > ) = > { 设置状态(true);
return aPromise.finally(() => > 设置状态(false));
};
return [isLoading, load] as [
boolean,
(aPromise: Promise<any>) => Promise<any>,
];
}
如果你编写大量自定义钩子,一个自动为元组类型的辅助函数也会很有帮助:
函数 tuplify < T 扩展任何[] > (...元素: T) {
返回元素;
}
function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // type is (number | (() => void))[]
}
function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return tuplify(numberValue, functionValue); // 类型是 [number, () = > void]
}
请注意,React 团队建议,自定义 Hook 如果返回两个以上的值,应使用适当的对象而不是元组。
更多 Hooks TypeScript 阅读:
- https://medium.com/@jrwebdev/react-hooks-in-typescript-88fce7001d0d
- https://fettblog.eu/typescript-react/hooks/#useref
- https://github.com/gibbok/typescript-book
如果你正在编写一个 React Hooks 库,不要忘记你还应该为用户公开你的类型以供使用。
Example React Hooks TypeScript Library:
- https://github.com/mweststrate/use-st8
- https://github.com/palmerhq/the-platform
- https://github.com/sw-yx/hooks
类组件
在 TypeScript 中,React.Component 是一个泛型类型(也就是 React.Component<PropType, StateType>),因此你需要为它提供(可选的)属性和状态类型参数:
type MyProps = {
// using `interface` is also ok
message: string;
};
type MyState = {
count: number; // like this
};
class App extends React.Component<MyProps, MyState> {
state: MyState = {
// optional second annotation for better type inference
count: 0,
};
render() {
return (
<div>
{this.props.message} {this.state.count}
</div>
);
}
}
不要忘记,你可以导出/导入/扩展这些类型/接口以便重用。
为什么要对 state 注释两次?
给 state 类属性添加注解不是严格必要的,但它可以在访问 this.state 时提供更好的类型推断,也有助于初始化状态。
这是因为它们以两种不同的方式工作,第二个泛型类型参数将允许 this.setState() 正确工作,因为该方法来自基类,但是在组件内部初始化 state 会覆盖基类实现,所以你必须确保告诉编译器你实际上并没有做任何不同的事情。
无需 只读
你经常会看到示例代码中使用 readonly 来标记 props 和 state 为不可变:
type MyProps = {
只读消息: 字符串;
};
类型 MyState = { 只读 count: 数字;
};
这是没有必要的,因为 React.Component<P,S> 已经将它们标记为不可变的。(查看 PR 和讨论!)
类方法:像平常一样操作,但要记住,你的函数的任何参数也需要指定类型:
class App extends React.Component<{ message: string }, { count: number }> {
state = { count: 0 };
render() {
return (
<div onClick={() => this.increment(1)}>
{this.props.message} {this.state.count}
</div>
);
}
increment = (amt: number) => {
// like this
this.setState((state) => ({
count: state.count + amt,
}));
};
}
类属性:如果你需要声明类属性以供以后使用,请直接在类的body中定义它们,而无需初始赋值:
class App extends React.Component<{
message: string;
}> {
pointer: number; // like this
componentDidMount() {
this.pointer = 3;
}
render() {
return (
<div>
{this.props.message} and {this.pointer}
</div>
);
}
}
输入 getDerivedStateFromProps
在你开始使用 getDerivedStateFromProps 之前,请先阅读 文档 和 你很可能不需要派生状态。派生状态可以使用 hooks 来实现,这也有助于设置记忆化。
这里有几种方式可以注释 getDerivedStateFromProps
- 如果您明确地键入了派生状态,并且想要确保
getDerivedStateFromProps的返回值符合它。
class Comp extends React.Component<Props, State> {
static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | null {
//
}
}
- 当你希望函数的返回值决定你的状态时。
class Comp extends React.Component<
Props,
ReturnType<(typeof Comp)["getDerivedStateFromProps"]>
> {
static getDerivedStateFromProps(props: Props) {}
}
- 当你想要使用其他状态字段和记忆化来获取派生状态时
type CustomValue = any;
interface Props {
propA: CustomValue;
}
interface DefinedState {
otherStateField: string;
}
type State = DefinedState & ReturnType<typeof transformPropsToState>;
function transformPropsToState(props: Props) {
return {
savedPropA: props.propA, // save for memoization
derivedState: props.propA,
};
}
class Comp extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
otherStateField: "123",
...transformPropsToState(props),
};
}
static getDerivedStateFromProps(props: Props, state: State) {
if (isEqual(props.propA, state.savedPropA)) return null;
return transformPropsToState(props);
}
}
函数组件
从 React 19 开始,defaultProps 不再支持函数组件。请直接在参数列表中使用解构默认值 —— TypeScript 会自动将该 prop 推断为可选:
type GreetProps = { age?: number };
const Greet = ({ age = 21 }: GreetProps) => {
// ...
};
如果您更愿意单独声明默认值(例如,为了在组件之间共享它们),可以将它们提取到一个常量中,并在解构时展开它们:
type GreetProps = { age?: number };
const defaultProps = { age: 21 } satisfies GreetProps;
const Greet = ({ age = defaultProps.age }: GreetProps) => {
// ...
};
在 React 19 中,对函数组件设置
Greet.defaultProps = { age: 21 }是无效的——该值在运行时会被忽略,并且FunctionComponent不再为其进行类型标注。
类组件
类组件仍然支持 static defaultProps。推荐的方法是将带有默认键的 props 类型设为必填,并让 LibraryManagedAttributes(由 JSX 自动应用)在调用处将它们设为可选:
type GreetProps = {
age: number;
};
class Greet extends React.Component<GreetProps> {
static defaultProps = {
age: 21,
};
render() {
return <div>Hello, I am {this.props.age}</div>;
}
}
// Type-checks — `age` is optional at the call site thanks to defaultProps.
const el = <Greet />;
React.JSX.LibraryManagedAttributes 对库作者的细微差别
如果你导出 GreetProps 给消费者,age 将会显示为必需,尽管 defaultProps 在调用时使其可选。GreetProps 是 内部 合同——有一个单独的 外部 合同,JSX 通过 React.JSX.LibraryManagedAttributes 计算。你可以显式计算它:
// 内部合约 — 不导出
类型 GreetProps = { age: 数字;
};
class Greet 扩展自 React.Component < GreetProps > { static defaultProps = { age: 21 } }
}
// 外部合约导出类型 ApparentGreetProps = React.JSX.LibraryManagedAttributes<
typeof Greet,
问候属性
>;
对于大多数应用程序,这不是必需的——只有那些重新导出 props 类型的库作者才容易遇到这个问题。
输入组件属性
这是为熟悉 TypeScript 的 React 开发者提供的基本入门指南和参考。
基本属性类型示例
在 React TypeScript 应用中你可能会使用的 TypeScript 类型列表:
type AppProps = {
message: string;
count: number;
disabled: boolean;
/** array of a type! */
names: string[];
/** string literals to specify exact string values, with a union type to join them together */
status: "waiting" | "success";
/** an object with known properties (but could have more at runtime) */
obj: {
id: string;
title: string;
};
/** array of objects! (common) */
objArr: {
id: string;
title: string;
}[];
/** any non-primitive value - can't access any properties (NOT COMMON but useful as placeholder) */
obj2: object;
/** an interface with no required properties - (NOT COMMON, except for things like `React.Component<{}, State>`) */
obj3: {};
/** a dict object with any number of properties of the same type */
dict1: {
[key: string]: MyTypeHere;
};
dict2: Record<string, MyTypeHere>; // equivalent to dict1
/** function that doesn't take or return anything (VERY COMMON) */
onClick: () => void;
/** function with named prop (VERY COMMON) */
onChange: (id: number) => void;
/** function type syntax that takes an event (VERY COMMON) */
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/** alternative function type syntax that takes an event (VERY COMMON) */
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
/** any function as long as you don't invoke it (not recommended) */
onSomething: Function;
/** an optional prop (VERY COMMON!) */
optional?: OptionalType;
/** when passing down the state setter function returned by `useState` to a child component. `number` is an example, swap out with whatever the type of your state */
setState: React.Dispatch<React.SetStateAction<number>>;
};
object 作为非原始类型
object 是 TypeScript 中常见的误解来源。它并不意味着“任何对象”,而是意味着“任何非原始类型”,也就是说它表示除了 number、bigint、string、boolean、symbol、null 或 undefined 之外的任何东西。
在 React 中,给“任何非原始值”指定类型很可能不是你应该经常做的事情,这意味着你可能不会经常使用 object。
空接口,{} 和 Object
一个空接口、{} 和 Object 都表示“任何非空值”——而不是你可能认为的“一个空对象”。使用这些类型是常见的误解来源,不推荐使用。
interface AnyNonNullishValue {} // equivalent to `type AnyNonNullishValue = {}` or `type AnyNonNullishValue = Object`
let value: AnyNonNullishValue;
// these are all fine, but might not be expected
value = 1;
value = "foo";
value = () => alert("foo");
value = {};
value = { foo: "bar" };
// these are errors
value = undefined;
value = null;
有用的 React 属性类型示例
适用于接受其他 React 组件作为 props 的组件。
export declare interface AppProps {
children?: React.ReactNode; // best, accepts everything React can render
childrenElement: React.JSX.Element; // A single React element
style?: React.CSSProperties; // to pass through style props
onChange?: React.FormEventHandler<HTMLInputElement>; // form events! the generic parameter is the type of event.target
// more info: https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/patterns_by_usecase/#wrappingmirroring
props: Props & React.ComponentPropsWithoutRef<"button">; // to impersonate all the props of a button element and explicitly not forwarding its ref
props2: Props & React.ComponentPropsWithRef<MyButtonWithForwardRef>; // to impersonate all the props of MyButtonForwardedRef and explicitly forwarding its ref
}
React.JSX.Element 与 React.ReactNode 区别?
引用 @ferdaber:更技术性的解释是,有效的 React 节点并不等同于 React.createElement 返回的内容。无论组件最终渲染什么,React.createElement 总是返回一个对象,该对象符合 React.JSX.Element 接口,但 React.ReactNode 是组件所有可能返回值的集合。
React.JSX.Element->React.createElement的返回值React.ReactNode-> 组件的返回值
更多讨论:ReactNode 与 React.JSX.Element 不重叠的情况
类型还是接口?
你可以使用类型(Types)或接口(Interfaces)来定义 Props 和 State,所以自然而然地会产生一个问题——你应该使用哪个?
摘要
在需要类型之前使用接口 - orta。
更多建议
这里有一个有用的经验法则:
-
在编写库或第三方环境类型定义时,总是使用
interface来定义公共 API,因为如果某些定义缺失,这允许使用者通过 声明合并 来扩展它们。 -
考虑为你的 React 组件的 Props 和 State 使用
type,这样更一致,也因为它限制更多。
你可以在 Interface vs Type alias in TypeScript 2.7 中阅读更多关于这一经验法则背后的推理。
TypeScript 手册现在还包括关于 类型别名和接口之间的差异 的指南。
注意:在大规模应用中,出于性能原因更倾向于使用接口(参见微软官方关于此的说明),但对此需持保留态度
类型对于联合类型很有用(例如 type MyType = TypeA | TypeB),而接口更适合声明字典的结构,然后 implementing 或 extending 它们。
类型与接口的实用表
这是一个微妙的话题,别太纠结于此。这里有一个方便的表格:
| 方面 | 类型 | 接口 |
|---|---|---|
| 可以描述功能 | ✅ | ✅ |
| 可以描述构造函数 | ✅ | ✅ |
| 可以描述元组 | ✅ | ✅ |
| 接口可以扩展它 | ⚠️ | ✅ |
| 类可以继承它 | 🚫 | ✅ |
类可以实现它 (implements) |
⚠️ | ✅ |
| 可以与同类的另一个相交 | ✅ | ⚠️ |
| 可以与同类创建联合 | ✅ | 🚫 |
| 可用于创建映射类型 | ✅ | 🚫 |
| 可以通过映射类型进行映射 | ✅ | ✅ |
| 在错误消息和日志中展开 | ✅ | 🚫 |
| 可以增强 | 🚫 | ✅ |
| 可以递归 | ⚠️ | ✅ |
⚠️ 在某些情况下
(来源:Karol Majewski)
从属性派生状态
在你开始使用 getDerivedStateFromProps 之前,请先浏览 文档 和 你可能不需要派生状态。派生状态可以通过 hooks 轻松实现,同时 hooks 也可以帮助轻松设置记忆化。
这里有几种方式可以注释 getDerivedStateFromProps
- 如果您明确地键入了派生状态,并且想要确保
getDerivedStateFromProps的返回值符合它。
class Comp extends React.Component<Props, State> {
static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | null {
//
}
}
- 当你希望函数的返回值决定你的状态时。
class Comp extends React.Component<
Props,
ReturnType<(typeof Comp)["getDerivedStateFromProps"]>
> {
static getDerivedStateFromProps(props: Props) {}
}
- 当你想要使用其他状态字段和记忆化来获取派生状态时
type CustomValue = any;
interface Props {
propA: CustomValue;
}
interface DefinedState {
otherStateField: string;
}
type State = DefinedState & ReturnType<typeof transformPropsToState>;
function transformPropsToState(props: Props) {
return {
savedPropA: props.propA, // save for memoization
derivedState: props.propA,
};
}
class Comp extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
otherStateField: "123",
...transformPropsToState(props),
};
}
static getDerivedStateFromProps(props: Props, state: State) {
if (isEqual(props.propA, state.savedPropA)) return null;
return transformPropsToState(props);
}
}
形式与事件
如果性能不是问题(通常也不是!),内联处理程序是最简单的,因为你可以直接使用 类型推断和上下文类型:
const el = (
<button
onClick={(event) => {
/* event will be correctly typed automatically! */
}}
/>
);
但是如果你需要单独定义你的事件处理程序,IDE 工具在这里真的很有用,因为 @type 定义提供了丰富的类型信息。输入你要查找的内容,通常自动补全会帮助你。下面是表单事件的 onChange 的示例:
type State = {
text: string;
};
class App extends React.Component<Props, State> {
state = {
text: "",
};
// typing on RIGHT hand side of =
onChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ text: e.currentTarget.value });
};
render() {
return (
<div>
<input type="text" value={this.state.text} onChange={this.onChange} />
</div>
);
}
}
与其使用 React.FormEvent<> 和 void 来键入参数和返回值,你也可以将类型直接应用到事件处理程序本身(由 @TomasHubelbauer 贡献):
// typing on LEFT hand side of =
onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ text: e.currentTarget.value });
};
为什么有两种方法做同一件事?
第一种方法使用推断的方法签名 (e: React.FormEvent < HTMLInputElement > ): void ,而第二种方法则强制使用由 @types/react 提供的委托类型。因此 React.ChangeEventHandler < > 只是 " @types/react 所认可的 " 类型,而你可以将推断方法视为更 手工制作 。无论哪种方式,这都是一个值得了解的好模式。更多内容请参阅我们的 Github PR 。
从 React v19.2.10 开始,
FormEvent和FormEventHandler已被弃用,应替换为SubmitEvent和SubmitEventHandler。老的事件类型仍然可以使用,但会触发弃用消息。
在表单中使用非受控组件时输入 onSubmit
如果你并不特别在意事件的类型,你可以直接使用 React.SyntheticEvent。如果你的目标表单有自定义命名的输入项,你可以使用类型断言:
<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>
当然,如果你正在制作任何类型的重要表单,你应该使用 Formik 或 React Hook Form,它们是用 TypeScript 编写的。
事件类型列表
| 事件类型 | 描述 |
|---|---|
| 动画事件 | CSS 动画。 |
| ChangeEvent | 修改 <input>、<select> 和 <textarea> 元素的值。 |
| 剪贴板事件 | 使用复制、粘贴和剪切事件。 |
| CompositionEvent | 由于用户间接输入文本而发生的事件(例如,根据浏览器和电脑的设置,如果您想在美式键盘上输入日语,可能会出现一个带有额外字符的弹出窗口) |
| 拖放事件 | 使用指针设备(例如鼠标)进行的拖放交互。 |
| FocusEvent | 当元素获得或失去焦点时发生的事件。 |
| FormEvent | 每当表单或表单元素获得/失去焦点、表单元素的值被更改或表单被提交时发生的事件。 |
| InvalidEvent | 当输入的有效性限制失败时触发(例如 <input type="number" max="10">,有人输入数字 20 时)。 |
| KeyboardEvent | 用户与键盘的交互。每个事件描述一次单独的按键交互。 |
| 输入事件 | 在 <input>、<select> 和 <textarea> 的值更改之前发生的事件。 |
| 鼠标事件 | 由于用户与指针设备(例如鼠标)交互而发生的事件 |
| PointerEvent | 由于用户与各种指点设备(如鼠标、笔/触控笔、触摸屏)的交互而发生的事件,同时也支持多点触控。除非你为较旧的浏览器(IE10 或 Safari 12)开发,否则推荐使用指针事件。继承自 UIEvent。 |
| TouchEvent | 由于用户与触摸设备交互而发生的事件。继承自 UIEvent。 |
| TransitionEvent | CSS 过渡。并非所有浏览器完全支持。继承自 UIEvent |
| UIEvent | 鼠标、触摸和指针事件的基础事件。 |
| WheelEvent | 在鼠标滚轮或类似输入设备上滚动。(注意:wheel 事件不应与 scroll 事件混淆) |
| SyntheticEvent | 所有上述事件的基础事件。在不确定事件类型时应使用此事件 |
上下文
基本示例
这是一个创建包含活动主题的上下文的基本示例。
import { createContext } from "react";
type ThemeContextType = "light" | "dark";
const ThemeContext = createContext<ThemeContextType>("light");
通过将上下文本身渲染为提供者来包装需要上下文的组件。在 React 19 中,可以直接渲染上下文对象——你不再需要 <ThemeContext.Provider>:
import { useState } from "react";
const App = () => {
const [theme, setTheme] = useState<ThemeContextType>("light");
return (
<ThemeContext value={theme}>
<MyComponent />
</ThemeContext>
);
};
<ThemeContext.Provider value={theme}>仍然有效,行为完全相同——它只是旧的写法。
阅读带有 use 的上下文:
import { use } from "react";
const MyComponent = () => {
const theme = use(ThemeContext);
return <p>The current theme is {theme}.</p>;
};
useContext(ThemeContext)仍然可以使用。主要的区别在于use还可以解包一个 promise,并且可以在条件和循环中调用。
没有默认上下文值
如果你没有任何有意义的默认值,请指定 null:
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 value={currentUser}>
<MyComponent />
</CurrentUserContext>
);
};
现在上下文的类型可以是 null,如果你尝试访问 username 属性,你会注意到会出现 'currentUser' is possibly 'null' TypeScript 错误。你可以使用可选链来访问 username :
import { use } from "react";
const MyComponent = () => {
const currentUser = use(CurrentUserContext);
return <p>Name: {currentUser?.username}.</p>;
};
然而,最好不要必须检查 null,因为我们知道上下文不会是 null。实现这一点的一种方法是提供一个自定义钩子来使用上下文,如果上下文未提供,则抛出错误:
import { createContext, use } from "react";
interface CurrentUserContextType {
username: string;
}
const CurrentUserContext = createContext<CurrentUserContextType | null>(null);
const useCurrentUser = () => {
const currentUserContext = use(CurrentUserContext);
if (!currentUserContext) {
throw new Error(
"useCurrentUser has to be used within <CurrentUserContext>",
);
}
return currentUserContext;
};
在这里使用运行时类型检查的好处是,当提供者未正确包裹组件时,可以在控制台打印出清晰的错误信息。现在可以在不检查 null 的情况下访问 currentUser.username:
const MyComponent = () => {
const currentUser = useCurrentUser();
return <p>Username: {currentUser.username}.</p>;
};
类型断言作为一种替代
避免必须检查 null 的另一种方法是使用类型断言告诉 TypeScript 你知道上下文不是 null:
import { use } from "react";
const MyComponent = () => {
const currentUser = use(CurrentUserContext);
return <p>Name: {currentUser!.username}.</p>;
};
另一个选项是使用一个空对象作为默认值,并将其转换为预期的上下文类型:
const CurrentUserContext = createContext<CurrentUserContextType>(
{} as CurrentUserContextType,
);
你也可以使用非空断言来获得相同的结果:
const CurrentUserContext = createContext<CurrentUserContextType>(null!);
当你不知道该选择什么时,优先选择运行时检查和抛出,而不是类型断言。
forwardRef/createRef
关于 useRef,请查看Hooks 部分。
将 Ref 作为属性(推荐用于 React 19)
在 React 19 中,你可以在函数组件中直接将 ref 作为一个属性访问——无需 forwardRef 包装。
选项 1:继承原生元素的所有属性
使用 ComponentPropsWithRef 从原生元素继承所有属性。
import { ComponentPropsWithRef, useRef } from "react";
function MyInput(props: ComponentPropsWithRef<"input">) {
return <input {...props} />;
}
// Usage in parent component
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);
return <MyInput ref={inputRef} placeholder="Type here..." />;
}
选项 2:显式类型
如果你有自定义属性并且想要精细控制,你可以显式地为 ref 指定类型:
import { Ref, useRef } from "react";
interface MyInputProps {
placeholder: string;
ref: Ref<HTMLInputElement>;
}
function MyInput(props: MyInputProps) {
return <input {...props} />;
}
// Usage in parent component
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);
return <MyInput ref={inputRef} placeholder="Type here..." />;
}
阅读更多: 包装/镜像 HTML 元素
传统方法(React 19 之前)
forwardRef
对于 React 18 及更早版本,使用 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>
));
附注:你从 forwardRef 获取的 ref 是可变的,因此如果需要,你可以对其进行赋值。
这是故意为之。如果必须,你可以让它不可变——如果你想确保没人重新分配它,可以分配React.Ref:
从 "react" 导入 { forwardRef, ReactNode, Ref };
接口 Props { children?: ReactNode;
类型: "提交" | "按钮";
}
export const FancyButton = forwardRef(
(
props: Props, ref: Ref<HTMLButtonElement>, // <-- 显式不可变 ref 类型
) = > (
< 按钮 ref={ref} className="MyClassName" type={props.type} >
{props.children}
</button>
),
);
如果你需要获取一个转发 ref 的组件的属性,请使用 ComponentPropsWithRef。
创建引用
createRef 主要用于类组件。函数组件通常依赖 useRef。
import { createRef, PureComponent } from "react";
class CssThemeProvider extends PureComponent<Props> {
private rootRef = createRef<HTMLDivElement>();
render() {
return <div ref={this.rootRef}>{this.props.children}</div>;
}
}
带引用的通用组件
通用组件通常需要手动处理 ref,因为它们的通用性质阻止了自动类型推断。主要方法如下:
在 这篇文章 中阅读更多内容。
选项 1:包装组件
最直接的方法是通过 props 手动处理 refs:
interface 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={() => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
选项 2:重新声明 forwardRef
对于带有泛型的真正 forwardRef 行为,请扩展模块声明:
// Redeclare forwardRef to support generics
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;
}
// Now you can use forwardRef with generics normally
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={() => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
export const ClickableList = forwardRef(ClickableListInner);
选项 3:调用签名
如果你既需要通用支持,又需要具有完全类型推断的正确 forwardRef 行为,你可以使用调用签名:
// Add to your type definitions (e.g. in `index.d.ts` file)
interface ForwardRefWithGenerics extends React.FC<WithForwardRefProps<Option>> {
<T extends Option>(
props: WithForwardRefProps<T>,
): ReturnType<React.FC<WithForwardRefProps<T>>>;
}
export const ClickableListWithForwardRef: ForwardRefWithGenerics =
forwardRef(ClickableList);
鸣谢: https://stackoverflow.com/a/73795494
:::注意 选项 1 通常就足够且更清晰。当你特别需要 forwardRef 行为时使用选项 2。对于需要同时使用泛型和完整 forwardRef 类型推断的高级库场景,使用选项 3。:::
附加资源
门户
使用 ReactDOM.createPortal:
const modalRoot = document.getElementById("modal-root") as HTMLElement;
// assuming in your html file has a div with id 'modal-root';
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);
}
}
使用钩子
同上,但使用钩子
从 "react" 导入 { useEffect, useRef, ReactNode }
从 "react-dom" 导入 { createPortal };
const modalRoot = document.querySelector("#modal-root") as HTMLElement;
类型 ModalProps = { 子元素: ReactNode;
};
function 模态框({ children }: ModalProps) {
// 使用 ref 仅创建一次 div 元素 const elRef = useRef < HTMLDivElement | null > (null);
if (!elRef.current) elRef.current = document.createElement("div");
useEffect(() = > { const el = elRef.current!; // 非空断言,因为它永远不会为 null modalRoot.appendChild(el);
返回 () = > {
modalRoot.removeChild(el);
};
}, []);
返回 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: "papayawhip" }}
onClick={() => setShowModal(false)}
>
close
</button>
</div>
</Modal>
)}
<button onClick={() => setShowModal(true)}>show Modal</button>
// rest of your app
</div>
);
}
示例背景
此示例基于 React 文档中的 通过门户事件冒泡 示例。
错误边界
选项 1:使用 react-error-boundary
React-error-boundary - 是一个轻量级的软件包,适用于这种场景,并内置了 TS 支持。这种方法还可以让你避免使用现在不太流行的类组件。
选项 2:编写自定义错误边界组件
如果你不想为此添加新的 npm 包,你也可以自己编写 ErrorBoundary 组件。
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 {
// Update state so the next render will show the fallback UI.
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;
Concurrent React
并发 React API(Suspense、useTransition、useDeferredValue、startTransition、use)允许你在 React 在后台渲染工作或等待数据时保持 UI 的响应性。自 React 18 起它们都很稳定,并在 React 19 中获得了额外的功能。
Suspense
Suspense 允许你在子组件等待某些内容时声明性地显示一个回退——通常是使用 use(promise) 展开的数据、一个懒加载组件,或者服务器上的流式边界。
import { Suspense } from "react";
const UserProfile = ({ userPromise }: { userPromise: Promise<User> }) => {
const user = use(userPromise);
return <p>Hello, {user.name}!</p>;
};
const App = ({ userPromise }: { userPromise: Promise<User> }) => (
<Suspense fallback={<p>Loading...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
SuspenseProps 的类型为 { children?: ReactNode; fallback?: ReactNode }。fallback 可以是任何 ReactNode,包括 null。
use
use 读取上下文或 Promise 的值。与 useContext 不同,它可以在条件语句和循环中调用,并且可以与 Promise 的 Suspense 集成。
import { use } from "react";
const Comments = ({
commentsPromise,
}: {
commentsPromise: Promise<Comment[]>;
}) => {
// Suspends until the promise resolves; throws to the nearest <Suspense>.
const comments = use(commentsPromise);
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
};
这个 promise 通常由父组件创建并传递 —— 不要在组件内部创建,否则每次渲染都会创建一个新的 promise。
useTransition
useTransition 将状态更新标记为非紧急,因此在渲染时,React 可以保持打字、滚动和其他紧急输入的响应性。
import { useState, useTransition } from "react";
const TabSwitcher = () => {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState<"posts" | "comments">("posts");
const selectTab = (next: "posts" | "comments") => {
startTransition(() => {
setTab(next);
});
};
return (
<>
<button disabled={isPending} onClick={() => selectTab("posts")}>
Posts
</button>
<button disabled={isPending} onClick={() => selectTab("comments")}>
Comments
</button>
{tab === "posts" ? <Posts /> : <Comments />}
</>
);
};
异步过渡(React 19)
在 React 19 中,传递给 startTransition 的函数可以是异步的。这是 Actions 的基础,也是 useActionState 和 <form action> 调度其挂起状态的方式。
const [isPending, startTransition] = useTransition();
const onSubmit = () => {
startTransition(async () => {
await saveDraft(content);
setSavedAt(new Date());
});
};
isPending 在异步回调的整个期间都会保持为 true,包括等待的工作。
useDeferredValue
useDeferredValue 允许你延迟重新渲染计算开销较大的 UI 部分,这样紧急的更新(输入文本时)可以优先执行。
import { useDeferredValue, useState } from "react";
const SearchPage = () => {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{/* SearchResults re-renders with deferredQuery, lagging behind input */}
<SearchResults query={deferredQuery} />
</>
);
};
initialValue(React 19)
React 19 添加了一个可选的第二个参数:在延迟值跟上之前,初始渲染时使用的值。在 SSR/流式渲染时很有用,当你想显示一个已知的初始值而不是最新值时。
const deferredQuery = useDeferredValue(query, "");
startTransition(独立使用)
startTransition 也可以直接从 react 导出,以便在组件外使用 —— 例如,在非 React 代码的事件处理器或第三方存储中使用。
import { startTransition } from "react";
store.subscribe(() => {
startTransition(() => {
forceRender();
});
});
独立版本不提供 isPending 标志——如果需要该标志,请使用 hook。
另请参阅
我的问题在这里没有得到回答!
- 本文标题:react - 针对有经验的 React 开发者开始使用 TypeS
- 本文链接:https://cn121.com/manual/typescript-cheatsheets-react.html
- 原项目:typescript-cheatsheets/react 版权归原作者 typescript-cheatsheets 及贡献者所有
- 收录信息:本站于 2026-10-16 收录本项目,本页所列协议与仓库指标均为收录当时的状态;该日期之后原项目的版本更新与协议变更,本页不作同步。
- 开源协议:收录时本项目采用 MIT(查看 LICENSE 原文),本站译文为其衍生内容;使用、修改、分发请以该仓库 LICENSE 原文为准。
- 站点出处:本文首发于 OneTwoOne,收录自 GitHub 开源项目 typescript-cheatsheets/react。
- 翻译说明:本页正文为人工智能生成内容——由机器翻译对原项目 README 初译、经程序校验排版,可能存在错漏,请以原项目文档为准。
- 引用声明:商业转载、第三方聚合或 AI 检索训练引用时,请务必保留以上来源出处、本文永久链接,以及原项目的版权声明与许可信息。
- 下架通道:若原项目此后变更或收紧了许可协议、或作者/权利人认为本站的收录方式(译文、排版适配、简介翻译等)超出其授权范围,请通过 xyd3302001@163.com 发送下架通知,并附上项目地址与本页链接。本站核实后将第一时间删除本页内容,或改为不复制原文的目录性收录;署名更正等其他要求可一并提出。