阅读 2709

【译】React, TypeScript 中 defaultProps 的类型解决

原文链接: React, TypeScript and defaultProps dilemma
github 的地址 欢迎 star!

前言

现在大型的前端项目中是很需要注意可维护性,易读性的,因此选择使用“安全的” JavaScript -- 即 Typescript 是非常有帮助的,你工作将会变得更简单,避免一些潜在的错误问题。Typescript 搭配 React 能轻松的创建你的应用,配置好 tsconfig.json,那么 Typescript 就会指导你写出健壮的代码。一切都是那么顺利,直到你遇到一个“大的”问题--处理组件中的 defaultProps

作者在 github 上发布了 rex-tils代码库,其中包含了这篇文章讨论的解决办法

这篇文章是基于 TypeScript 2.9 版本以及开启严格模式进行的。我将会演示这个问题以及组件中怎么解决它

一个 Button 组件

我们开始定义一个 Button 组件,遵循以下的 API 实现:

Button API:

  • onClick (点击事件)
  • color (定义按钮颜色)
  • type (按钮的类型有 ‘button’ 或者 ‘submit’) 我们将 color 和 type 定义为可选项,用 defaultProps 定义他们的默认值(组件的使用者可能不会添加这些参数)
import {MouseEvent, Component} from 'react';
import * as React from 'react';

type Props = {
	onClick(e: MouseEvent<HTMLElement>): void;
	color?: 'blue' | 'green' | 'red';
	type?: 'button' | 'submit';
}
class Button extends Component<Props> {
	static defaultProps = {
		color: 'blue',
		type: 'button'
	};
	render() {
		const {onClick: handleClick, color, type, children} = this.props;
		return (
			<button
				type={type}
				style={{color}}
				onClick={handleClick}
			>
				{children}
			</button>
		);
	}
}
export default Button;
复制代码

Button 组件实现

现在,当我们在其他组件中使用 Button 时,编辑代码就能获取到相关的提示(可选项,必传的 Props)

但是,Button 组件的 defaultProps 属性没有被检查,因为类型检查器不能从泛型类扩展定义的静态属性中推断它的类型。

具体的解释:

  • 设置了任意类型为静态类型 static defaultProps
  • 定义两次相同的东西(类型和实现)

默认的 props 没有类型检查

可以通过分离 Props 提取出 color 和 type 的属性,然后用类型交叉把默认值映射为可选值。后面这步通过 TS 标准库中 Partial 类型来快速实现。

然后显示地指定 defaultProps 的类型是 DefaultProps,这样检查器就能检查 defaultProps。

组件的 defaultProps 属性类型检查的实现

我倾向于使用下面的方法,就是提取 defaultProps 和 initialState (如果组件有 state)来分离状态类型,这样做另外的好处——从实现里面可以获取类型的明确定义,减少了定义 props 的模板,只保留核心必选的功能。

接下来在组件里面加入一点复杂的逻辑。 需求是,不希望只使用 css 内嵌样式(这是反设计模式以及性能糟糕的),而是能基于 color 属性,生成具有一些预定义的 css 类。

定义了 resolveColorTheme函数,接受 color 的参数,返回自定义的 className。

但是,像上面那样会有一个编译错误的!

TS Error:
Type 'undefined' is not assignable to type '"blue" | "green" | "red"'
复制代码

可选的 props 导致的编译错误

为什么?color 是可选的,编译启用的是严格模式,而联合类型扩展存在 undefined/void 类型,但是函数不接受 undefined。

怎么修复这个价值很高的问题呢?

TypeScript 2.9 中提供了 4 中方法修复它:

  • 非空断言语句(Non-null assertion operator)
  • 组件类型重置(Component type casting)
  • 高阶组件定义 defaultProps
  • Props getter 函数

1. 非空断言语句

这个方法是显而易见的,就是明确告诉类型检查器,这不会是 null 或者 undefined,通过!操作符实现:

在 render 方法中使用非空断言语句

对于简单的用例(props 属性很少的,仅在 render 方法接受特定的 props 的用例)这样做没问题的,随着业务的增长,这样的方法会加剧你组件的混乱,不可读。你需要花费大量时间检查哪个 props 被定义为 defaultProps,占用了开发者大量时间,这样也容易导致错误

2. 组件类型重置

那么怎么解决上一种问题的局限性呢?我们通过创建一个匿名类,断言它的类型为组合类型,设置 defaultProps 单个的类型(以及只读类型限制),如下:

这能解决我们当前的问题,但感觉是怪异的。

在 TypeScript 2.8 版本介绍了一种高级类型- 条件类型(conditional types)

T extends U ? X : Y
// 表示 T 如果继承自 U,那么它的类型就是 X,否则就是 Y
复制代码

3. 高阶函数定义 defaultProps

定义一个工厂函数/高阶函数,用于 defaultProps 和 条件类型的 props 合并。

其中 withDefaultProps 就是一个高阶函数,使用它,你不用明确地使用 React 的接口定义 defaultProps,另外如果不需要检查 defaultProps,可以删除上面代码中 type DefaultProps 。 但是要注意,它不能用于泛型组件,像这样:

你在泛型组件上使用了高阶函数(withDefaultProps 函数),会导致它的泛型丢失,

4. props getter 函数

使用工厂/闭包模式来实现条件类型映射。

像使用了 withDefaultProps 函数一样,利用了类型映射结构,不过没有将 defaultProps 映射为可选的,因为在实现组件时它不是可选的。

createPropsGetter 函数创建了一个闭包,通过泛型参数存储/推断出 defaultProps 类型。然后该函数返回了带有 defaultProps 的 props,从 TS 运行时角度看,它返回的 props 和我们传递的是相同的,因此 React 标准的 API就能获取运行时 props 的获取/解析。 如下实现:

Button component with properly typed defaultProps and implementation props
Button 组件的 defaultProps 类型检查以及 props 实现

更进一步的解释:

createPropsGetter 实现组件类型的流程

到此为止,最终解决方案涵盖了所有上面的问题:

  • 不需要使用非空断言语句来避开类型检查
  • 不需要将组件强制间接地转换为其他类型
  • 不需要再次创建组件,从而不会再进程中丢失类型
  • 泛型组件也能使用
  • 易于推理,TypeScript 3.0 版本支持

如果有错误或者不严谨的地方,请务必给予指正,十分感谢!

参考

  1. TS 官网高级类型
  2. TS 一些工具泛型的使用及其实现