故障排除手册:类型
⚠️ 您是否阅读过 TypeScript 常见问题解答?您的答案可能就在那里!
遇到奇怪的类型错误?你并不孤单。这是在 React 中使用 TypeScript 最困难的部分。要有耐心——毕竟你正在学习一门新的语言。但是,你在这方面越熟练,你与编译器“对抗”的时间就越少,编译器为你工作的时间就越多!
尽量避免使用any
进行类型化,以体验 TypeScript 的全部优势。相反,让我们尝试熟悉一些解决这些问题的常见策略。
联合类型和类型守卫
联合类型对于解决一些类型问题非常方便
class App extends React.Component<
{},
{
count: number | null; // like this
}
> {
state = {
count: null,
};
render() {
return <div onClick={() => this.increment(1)}>{this.state.count}</div>;
}
increment = (amt: number) => {
this.setState((state) => ({
count: (state.count || 0) + amt,
}));
};
}
**类型守卫**: 有时联合类型在一个区域解决了问题,但在下游创建了另一个问题。如果A
和B
都是对象类型,则A | B
不是“A或B”,而是“A或B或两者兼而有之”,如果您期望它是前者,这会导致一些混淆。学习如何编写检查、守卫和断言(另请参阅下面的条件渲染部分)。例如
interface Admin {
role: string;
}
interface User {
email: string;
}
// Method 1: use `in` keyword
function redirect(user: Admin | User) {
if ("role" in user) {
// use the `in` operator for typeguards since TS 2.7+
routeToAdminPage(user.role);
} else {
routeToHomePage(user.email);
}
}
// Method 2: custom type guard, does the same thing in older TS versions or where `in` isnt enough
function isAdmin(user: Admin | User): user is Admin {
return (user as any).role !== undefined;
}
方法 2 也称为 用户定义类型守卫,对于可读性强的代码非常方便。这就是 TS 本身使用typeof
和instanceof
细化类型的方式。
如果您需要if...else
链或switch
语句,它应该“正常工作”,但如果您需要帮助,请查找 区分联合类型。(另请参阅:Basarat 的文章)。这对于为useReducer
或 Redux 类型化 reducer 非常方便。
可选类型
如果组件有一个可选的 prop,请添加一个问号并在解构期间赋值(或使用defaultProps)。
class MyComponent extends React.Component<{
message?: string; // like this
}> {
render() {
const { message = "default" } = this.props;
return <div>{message}</div>;
}
}
您还可以使用!
字符断言某项不是未定义的,但不建议这样做。
有什么要补充的吗?提交问题并提供您的建议!
枚举类型
我们建议尽可能避免使用枚举.
枚举有一些 已记录的问题(TS 团队 同意)。枚举的一个更简单的替代方案是只声明一个字符串字面量的联合类型
export declare type Position = "left" | "right" | "top" | "bottom";
如果您必须使用枚举,请记住 TypeScript 中的枚举默认为数字。您通常希望将它们用作字符串
export enum ButtonSizes {
default = "default",
small = "small",
large = "large",
}
// usage
export const PrimaryButton = (
props: Props & React.HTMLProps<HTMLButtonElement>
) => <Button size={ButtonSizes.default} {...props} />;
类型断言
有时您比 TypeScript 更了解您正在使用的类型比它认为的更窄,或者联合类型需要断言为更具体的类型才能与其他 API 一起使用,因此请使用as
关键字进行断言。这告诉编译器您比它更了解。
class MyComponent extends React.Component<{
message: string;
}> {
render() {
const { message } = this.props;
return (
<Component2 message={message as SpecialMessageType}>{message}</Component2>
);
}
}
请注意,您不能通过断言获得任何东西——基本上它仅用于细化类型。因此,它与“转换”类型不同。
您也可以在访问属性时断言属性不为空。
element.parentNode!.removeChild(element); // ! before the period
myFunction(document.getElementById(dialog.id!)!); // ! after the property accessing
let userID!: string; // definite assignment assertion... be careful!
当然,请尝试实际处理空值情况,而不是断言 :)
模拟名义类型
TS 的结构化类型很方便,直到它变得不方便为止。但是,您可以使用 类型标记
模拟名义类型
type OrderID = string & { readonly brand: unique symbol };
type UserID = string & { readonly brand: unique symbol };
type ID = OrderID | UserID;
我们可以使用伴随对象模式创建这些值
function OrderID(id: string) {
return id as OrderID;
}
function UserID(id: string) {
return id as UserID;
}
现在 TypeScript 将不允许您在错误的地方使用错误的 ID
function queryForUser(id: UserID) {
// ...
}
queryForUser(OrderID("foobar")); // Error, Argument of type 'OrderID' is not assignable to parameter of type 'UserID'
将来您可以使用unique
关键字进行标记。参见此 PR。
交集类型
将两种类型组合在一起可能很方便,例如,当您的组件应该镜像原生组件(如button
)的 props 时
export interface PrimaryButtonProps {
label: string;
}
export const PrimaryButton = (
props: PrimaryButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>
) => {
// do custom buttony stuff
return <button {...props}> {props.label} </button>;
};
Playground 在此处
您还可以使用交集类型为类似组件创建可重用的 props 子集
type BaseProps = {
className?: string,
style?: React.CSSProperties
name: string // used in both
}
type DogProps = {
tailsCount: number
}
type HumanProps = {
handsCount: number
}
export const Human = (props: BaseProps & HumanProps) => // ...
export const Dog = (props: BaseProps & DogProps) => // ...
确保不要将交集类型(它是**与**运算)与联合类型(它是**或**运算)混淆。
联合类型
此部分尚未编写(请贡献!)。同时,请参阅我们关于联合类型用例的 评论。
高级速查表还包含有关区分联合类型的信息,当 TypeScript 似乎没有按预期缩小您的联合类型时,这些信息很有用。
重载函数类型
特别是对于函数,您可能需要重载而不是联合类型。函数类型最常见的编写方式使用简写
type FunctionType1 = (x: string, y: number) => number;
但这不允许您进行任何重载。如果您有实现,您可以将它们与函数关键字一起放在一起
function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x): any {
// implementation with combined signature
// ...
}
但是,如果您没有实现并且只是编写.d.ts
定义文件,这也不会对您有所帮助。在这种情况下,您可以放弃任何简写并以旧式的方式编写它们。这里需要记住的关键是,就 TypeScript 而言,函数只是没有键的可调用对象
type pickCard = {
(x: { suit: string; card: number }[]): number;
(x: number): { suit: string; card: number };
// no need for combined signature in this form
// you can also type static properties of functions here eg `pickCard.wasCalled`
};
请注意,当您实现实际的重载函数时,实现将需要声明您将处理的组合调用签名,它不会为您推断。您可以在 DOM API 中轻松看到重载的示例,例如createElement
。
使用推断类型
依赖 TypeScript 的类型推断非常棒……直到您意识到需要一个推断的类型,并且必须返回并显式声明类型/接口以便您可以导出它们以供重用。
幸运的是,使用typeof
,您不必这样做。只需在任何值上使用它即可
const [state, setState] = useState({
foo: 1,
bar: 2,
}); // state's type inferred to be {foo: number, bar: number}
const someMethod = (obj: typeof state) => {
// grabbing the type of state even though it was inferred
// some code using obj
setState(obj); // this works
};
使用部分类型
在 React 中,处理切片状态和 props 很常见。同样,如果您使用Partial
泛型类型,您实际上不必返回并显式重新定义您的类型
const [state, setState] = useState({
foo: 1,
bar: 2,
}); // state's type inferred to be {foo: number, bar: number}
// NOTE: stale state merging is not actually encouraged in useState
// we are just demonstrating how to use Partial here
const partialStateUpdate = (obj: Partial<typeof state>) =>
setState({ ...state, ...obj });
// later on...
partialStateUpdate({ foo: 2 }); // this works
使用Partial
的一些注意事项
请注意,有些 TS 用户不同意使用Partial
,因为它在今天表现的方式。参见 此处关于上述示例的微妙陷阱,并查看关于 @types/react 为什么使用 Pick 而不是 Partial 的长时间讨论。
我需要的类型未导出!
这可能很烦人,但以下是一些获取类型的方法!
- 获取组件的 Prop 类型:使用
React.ComponentProps
和typeof
,并可选地Omit
任何重叠的类型
import { Button } from "library"; // but doesn't export ButtonProps! oh no!
type ButtonProps = React.ComponentProps<typeof Button>; // no problem! grab your own!
type AlertButtonProps = Omit<ButtonProps, "onClick">; // modify
const AlertButton = (props: AlertButtonProps) => (
<Button onClick={() => alert("hello")} {...props} />
);
您还可以使用 ComponentPropsWithoutRef
(而不是 ComponentProps)和 ComponentPropsWithRef
(如果您的组件专门转发 refs)
- 获取函数的返回类型:使用
ReturnType
// inside some library - return type { baz: number } is inferred but not exported
function foo(bar: string) {
return { baz: 1 };
}
// inside your app, if you need { baz: number }
type FooReturn = ReturnType<typeof foo>; // { baz: number }
实际上,您可以获取几乎所有公共内容:参见 Ivan Koshelev 的这篇博文
function foo() {
return {
a: 1,
b: 2,
subInstArr: [
{
c: 3,
d: 4,
},
],
};
}
type InstType = ReturnType<typeof foo>;
type SubInstArr = InstType["subInstArr"];
type SubInstType = SubInstArr[0];
let baz: SubInstType = {
c: 5,
d: 6, // type checks ok!
};
//You could just write a one-liner,
//But please make sure it is forward-readable
//(you can understand it from reading once left-to-right with no jumps)
type SubInstType2 = ReturnType<typeof foo>["subInstArr"][0];
let baz2: SubInstType2 = {
c: 5,
d: 6, // type checks ok!
};
- TS 还附带一个
Parameters
实用程序类型,用于提取函数的参数 - 对于任何更“自定义”的内容,
infer
关键字是它的基本构建块,但需要一些时间才能习惯。查看上述实用程序类型的源代码,以及 此示例以了解其思想。Basarat 也有一段关于infer
的不错视频。
我需要的类型不存在!
有什么比具有未导出类型的模块更烦人的?**未类型化**的模块!
在继续操作之前,请确保您已检查类型是否不存在于 DefinitelyTyped 或 TypeSearch 中
别担心!您可以通过多种方式解决此问题。
在所有内容上都使用any
一种更简单的方法是创建一个新的类型声明文件,例如typedec.d.ts
(如果你还没有的话)。确保 TypeScript 可以解析到该文件路径,方法是检查目录根目录下的tsconfig.json
文件中的include
数组。
// inside tsconfig.json
{
// ...
"include": [
"src" // automatically resolves if the path to declaration is src/typedec.d.ts
]
// ...
}
在这个文件中,为所需的模块(例如my-untyped-module
)添加declare
语法到声明文件中。
// inside typedec.d.ts
declare module "my-untyped-module";
如果你只需要它正常工作而没有错误,那么仅此一行代码就足够了。一种更简陋的、一劳永逸的方法是使用"*"
,这将为所有现有的和未来的未类型化模块应用Any
类型。
如果你的未类型化模块少于几个,这种解决方案作为一种变通方法效果很好。如果更多,那么你的手中就有一个定时炸弹。解决此问题的唯一方法是为这些未类型化模块定义缺失的类型,如下面的章节所述。
自动生成类型
你可以使用 TypeScript 以及--allowJs
和--declaration
选项来查看 TypeScript 对库类型的“最佳猜测”。
如果这效果不好,可以使用dts-gen
利用对象的运行时形状来准确枚举所有可用的属性。这通常非常准确,但该工具尚不支持抓取 JSDoc 注释以填充其他类型。
npm install -g dts-gen
dts-gen -m <your-module>
还有其他自动的 JS 到 TS 转换工具和迁移策略 - 请参阅我们的迁移速查表。
为导出的 Hook 编写类型
为 Hook 编写类型就像为纯函数编写类型一样。
以下步骤基于两个假设
- 你已如前文所述创建了一个类型声明文件。
- 你可以访问源代码 - 特别是直接导出你将使用的函数的代码。在大多数情况下,它将位于
index.js
文件中。通常,你需要至少两个类型声明(一个用于输入 Props,另一个用于返回值 Props)才能完全定义一个 Hook。假设你想要为以下结构的 Hook 编写类型,
// ...
const useUntypedHook = (prop) => {
// some processing happens here
return {
/* ReturnProps */
};
};
export default useUntypedHook;
那么你的类型声明很可能应该遵循以下语法。
declare module 'use-untyped-hook' {
export interface InputProps { ... } // type declaration for prop
export interface ReturnProps { ... } // type declaration for return props
export default function useUntypedHook(
prop: InputProps
// ...
): ReturnProps;
}
例如,useDarkMode Hook导出的函数遵循类似的结构。
// inside src/index.js
const useDarkMode = (
initialValue = false, // -> input props / config props to be exported
{
// -> input props / config props to be exported
element,
classNameDark,
classNameLight,
onChange,
storageKey = "darkMode",
storageProvider,
global,
} = {}
) => {
// ...
return {
// -> return props to be exported
value: state,
enable: useCallback(() => setState(true), [setState]),
disable: useCallback(() => setState(false), [setState]),
toggle: useCallback(() => setState((current) => !current), [setState]),
};
};
export default useDarkMode;
如注释所示,按照上述结构导出这些配置 Props 和返回值 Props 将导致以下类型导出。
declare module "use-dark-mode" {
/**
* A config object allowing you to specify certain aspects of `useDarkMode`
*/
export interface DarkModeConfig {
classNameDark?: string; // A className to set "dark mode". Default = "dark-mode".
classNameLight?: string; // A className to set "light mode". Default = "light-mode".
element?: HTMLElement; // The element to apply the className. Default = `document.body`
onChange?: (val?: boolean) => void; // Override the default className handler with a custom callback.
storageKey?: string; // Specify the `localStorage` key. Default = "darkMode". Set to `null` to disable persistent storage.
storageProvider?: WindowLocalStorage; // A storage provider. Default = `localStorage`.
global?: Window; // The global object. Default = `window`.
}
/**
* An object returned from a call to `useDarkMode`.
*/
export interface DarkMode {
readonly value: boolean;
enable: () => void;
disable: () => void;
toggle: () => void;
}
/**
* A custom React Hook to help you implement a "dark mode" component for your application.
*/
export default function useDarkMode(
initialState?: boolean,
config?: DarkModeConfig
): DarkMode;
}
为导出的组件编写类型
在为未类型化的类组件编写类型的情况下,方法几乎没有区别,除了声明类型后,你使用class UntypedClassComponent extends React.Component<UntypedClassComponentProps, any> {}
扩展类型,其中UntypedClassComponentProps
保存类型声明。
例如,sw-yx 关于 React Router 6 类型的 Gist对当时未类型化的 RR6 实现了类似的方法。
declare module "react-router-dom" {
import * as React from 'react';
// ...
type NavigateProps<T> = {
to: string | number,
replace?: boolean,
state?: T
}
//...
export class Navigate<T = any> extends React.Component<NavigateProps<T>>{}
// ...
有关为类组件创建类型定义的更多信息,你可以参考这篇文章。
TypeScript 的常见已知问题
React 开发人员经常遇到的 TS 没有解决方案的一些问题列表。不一定是仅限于 TSX。
TypeScript 在对象元素空值检查后不会缩小类型
参考:https://mobile.twitter.com/tannerlinsley/status/1390409931627499523。另请参阅https://github.com/microsoft/TypeScript/issues/9998
TypeScript 不允许你限制子元素的类型
无法保证此类 API 的类型安全。
<Menu>
<MenuItem/> {/* ok */}
<MenuLink/> {/* ok */}
<div> {/* error */}
</Menu>
来源:https://twitter.com/ryanflorence/status/1085745787982700544?s=20