React
React.FC
import React, { HTMLAttributes, PropsWithChildren } from "react";
interface IHelloProps extends HTMLAttributes<HTMLDivElement> {
name: string;
}
const Hello: React.FC<PropsWithChildren<IHelloProps>> = ({
name,
children,
...rest
}) => {
return (
<div>
<div {...rest}>{`Hello, ${name}!`}</div>
{children}
</div>
);
};
- 使用
PropsWithChildren
为IHelloProps
注入children
类型 使用
React.FC
声明组件,通过泛型参数传入组件Props
类型- 注意:
react@16
类型定义中React.FC
自带children
类型,无需额外处理(即可省略第 1 步)
- 注意:
- 若组件需要接受
html
属性,如className
、style
等,可以直接extends HTMLAttributes<HTMLDivElement>
,其中HTMLDivElement
可替换为所需要的类型,如HTMLInputElement
不推荐 React.FC?
Remove React.FC from Typescript template #8177
在这个 PR 里移除了 CRA 默认模板的 React.FC,主要有以下几点理由:
- 隐式定义了 children
- 无法支持泛型组件
- 挂载静态属性较为复杂,如
<Select.Option>
- defaultProps 存在问题
好处只有一点:
- 提供了返回值约束
所以是否使用 React.FC 可以自行选择,泛型组件、挂载静态属性属于低频场景,遇见了不用 React.FC 就是了~
至于 defaultProps 基本已经不会在业务代码中使用(使用默认值替代),并且最新的 React.FC 已经移除了内置 children
如果对于返回值有明确的类型要求,配置了 typescript 规则,那么可以使用 React.FC,其他时候可以直接定义 Props interface,如下所示:
import React, { HTMLAttributes, PropsWithChildren } from "react";
interface IHelloProps extends HTMLAttributes<HTMLDivElement> {
name: string;
}
const Hello = ({ name, children, ...rest }: PropsWithChildren<IHelloProps>) => {
return (
<div>
<div {...rest}>{`Hello, ${name}!`}</div>
{children}
</div>
);
};
React.forwardRef
React 提供了 forwardRef
函数用于转发 Ref,该函数也可传入泛型参数,如下:
import { forwardRef, PropsWithChildren } from "react";
interface IFancyButtonProps {
type: "submit" | "button";
}
export const FancyButton = forwardRef<
HTMLButtonElement,
PropsWithChildren<IFancyButtonProps>
>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));
React.ComponentProps
用于获取组件 Props 的工具泛型,与之类似的还有:
- React.ComponentPropsWithRef
- React.ComponentPropsWithoutRef
import { DatePicker } from "@douyinfe/semi-ui";
type SemiDatePikerProps = React.ComponentProps<typeof DatePicker>;
export const DisabledDatePicker: React.FC = () => {
const disabledDate: SemiDatePikerProps["disabledDate"] = (date) => {
// ...
};
return <DatePicker disabledDate={disabledDate} />;
};
使用第三方库组件时,不要使用具体 path 去引用类型(若第三方组件后续升级修改了内部文件引用路径,会出现错误)。
import { InputProps } from "@douyinfe/semi-ui/input"; // ×
import { InputProps } from "@douyinfe/semi-ui"; // √
若入口文件未暴露对应组件的相关类型声明,使用 React.ComponentProps
import { Input } from "@douyinfe/semi-ui";
type InputProps = React.ComponentProps<typeof Input>;
另外一个例子:
类型收窄
某些场景传入的参数为联合类型,需要基于一些手段将其类型收窄(Narrowing)。
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
// strs 为 string[]
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
// strs 为 string
console.log(strs);
}
}
使用 type predicates: is
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
思考一下 Lodash 的 isBoolean
/isString
/isArray
...等函数,再思考一下使用 isEmpty
有什么不对。
interface LoDashStatic {
isBoolean(value?: any): value is boolean;
isString(value?: any): value is string;
isArray(value?: any): value is any[];
isEmpty(value?: any): boolean; // 这里的定义会使得业务中时使用出现什么问题?
}
类型安全的 redux action
笔者不用 redux,此处仅做演示
TS Playground - An online editor for exploring TypeScript and JavaScript
interface ActionA {
type: "a";
a: string;
}
interface ActionB {
type: "b";
b: string;
}
type Action = ActionA | ActionB;
function reducer(action: Action) {
switch (action.type) {
case "a":
return console.info("action a: ", action.a);
case "b":
return console.info("action b: ", action.b);
}
}
reducer({ type: "a", a: "1" }); // √
reducer({ type: "b", b: "1" }); // √
reducer({ type: "a", b: "1" }); // ×
reducer({ type: "b", a: "1" }); // ×
- How to type Redux actions and Redux reducers in TypeScript?
- 更多收窄方式可参考官方文档 - TypeScript Documentation - Narrowing
多参数类型约束
以非常熟悉的 window.addEventListener
为例:
// e 为 MouseEvent
window.addEventListener("click", (e) => {
// ...
});
// e 为 DragEvent
window.addEventListener("drag", (e) => {
// ...
});
可以发现 addEventListener
的回调函数入参类型(event)会随着监听事件的不同而不同,addEventListener
的函数签名如下:
addEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
type
为泛型 K,约束在 WindowEventMap
的 key 范围内,再基于 K 从 WindowEventMap
推导出 ev 事件类型即可。
当然你也可以选择使用联合类型,就像 redux action 那样。
常用工具泛型
了解完 TypeScript 基础内容(keyof/in/extends/infer)后,可自行尝试实现内置工具泛型,实现一遍理解更深刻。
interface Person {
name: string;
age: number;
address?: string;
}
- Partial<T>。将所有字段变为 optional
type PartialPerson = Partial<Person>;
// ↓
type PartialPerson = {
name?: string | undefined;
age?: number | undefined;
address?: string | undefined;
};
- Required<T>。将所有字段变为 required
type RequiredPerson = Required<Person>;
// ↓
type RequiredPerson = {
name: string;
age: number;
address: string;
};
- Pick<T, K extends keyof T>。从 T 中取出部分属性 K
type PersonWithoutAddress = Pick<Person, "name" | "age">;
// ↓
type PersonWithoutAddress = {
name: string;
age: number;
};
- Omit<T, K extends keyof T>。从 T 中移除部分属性 K
type PersonWithOnlyAddress = Omit<Person, "name" | "age">;
// ↓
type PersonWithOnlyAddress = {
address?: string | undefined;
};
- Exclude<T, U>。从 T 中排除那些可分配给 U 的类型
该泛型实现需要掌握 Distributive Conditional Types
type T = Exclude<1 | 2, 1 | 3>; // -> 2
- Extract<T, U>。从 T 中提取那些可分配给 U 的类型
该泛型实现需要掌握 Distributive Conditional Types
type T = Extract<1 | 2, 1 | 3>; // -> 1
- Parameters。获取函数入参类型
declare function f1(arg: { a: number; b: string }): void;
type T = Parameters<typeof f1>;
// ↓
type T = [
arg: {
a: number;
b: string;
}
];
- ReturnType。获取函数返回值类型
declare function f1(): { a: number; b: string };
type T = ReturnType<typeof f1>;
// ↓
type T = {
a: number;
b: string;
};
- Record<K, T>。将 K 中所有的属性的值转化为 T 类型
把一个个工具泛型理解成函数,类型作为入参和返回值即可,通过 cmd + 左键点击具体工具泛型阅读具体实现也可。
- 工具泛型具体实现请阅读:TS 一些工具泛型的使用及其实现
- 更多内置工具泛型可参考:TypeScript Documentation - Utility Types
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。