forked from ascoders/weekly
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
141 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
## 1 引言 | ||
|
||
从 [@types/react](https://unpkg.com/browse/@types/[email protected]/index.d.ts) 源码中挖掘一些 Typescript 使用技巧吧。 | ||
|
||
## 2 精读 | ||
|
||
### 泛型 extends | ||
|
||
泛型可以指代可能的参数类型,但指代任意类型范围太模糊,当我们需要对参数类型加以限制,或者确定只处理某种类型参数时,就可以对泛型进行 extends 修饰。 | ||
|
||
问题:`React.lazy` 需要限制返回值是一个 `Promise<T>` 类型,且 `T` 必须是 React 组件类型。 | ||
|
||
方案: | ||
|
||
```typescript | ||
function lazy<T extends ComponentType<any>>( | ||
factory: () => Promise<{ default: T }> | ||
): LazyExoticComponent<T>; | ||
``` | ||
|
||
`T extends ComponentType` 确保了 T 这个类型一定符合 `ComponentType` 这个 React 组件类型定义,我们再将 T 用到 `Promise<{ default: T }>` 位置即可。 | ||
|
||
## 泛型 extends + infer | ||
|
||
如果有一种场景,需要拿到一个类型,这个类型是当某个参数符合某种结构时,这个结构内的一种子类型,就需要结合 泛型 extends + infer 了。 | ||
|
||
问题:`React.useReducer` 第一个参数是 Reducer,第二个参数是初始化参数,其实第二个参数的类型是第一个参数中回调函数第一个参数的类型,那我们怎么将这两个参数的关系联系到一起呢? | ||
|
||
方案: | ||
|
||
```typescript | ||
function useReducer<R extends Reducer<any, any>, I>( | ||
reducer: R, | ||
initializerArg: I & ReducerState<R>, | ||
initializer: (arg: I & ReducerState<R>) => ReducerState<R> | ||
): [ReducerState<R>, Dispatch<ReducerAction<R>>]; | ||
|
||
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> | ||
? S | ||
: never; | ||
``` | ||
|
||
`R extends Reducer<any, any>` 的意思在上面已经提过了,也就是 R 必须符合 `Reducer` 结构,也就是 `reducer` 必须符合这个结构,之后重点来了:`initializerArg` 利用 `ReducerState` 这个类型直接从 `reducer` 的类型 `R` 中将第一个回调参数挖了出来并返回。 | ||
|
||
`ReducerState` 定义中 `R extends Reducer<infer S, any> ? S : never` 的含义是:如果 R 符合 `Reducer<infer S, any>` 类型,则返回类型 `S`,这个 `S` 是 `Reducer<infer S>` 也就是 State 位置的类型,否则返回 `never` 类型。 | ||
|
||
所以 infer 表示待推断类型,是非常强大的功能,可以指定在任意位置代指其类型,并配合 extends 判断是否符合结构,可以使类型推断具备一定编程能力。 | ||
|
||
要用 extends 的另一个原因是,只有 extends 才能将结构描述出来,我们才能精确定义 infer 指代类型的位置。 | ||
|
||
### 类型重载 | ||
|
||
当一个类型拥有多种使用可能性时,可以采用类型重载定义复数类型,Typescript 作用时会逐个匹配并找到第一个满足条件的。 | ||
|
||
问题:`createElement` 第一个参数支持 FunctionComponent 与 ClassComponent,而且传入参数不同,返回值的类型也不同。 | ||
|
||
方案: | ||
|
||
```typescript | ||
function createElement<P extends {}>( | ||
type: FunctionComponent<P>, | ||
props?: (Attributes & P) | null, | ||
...children: ReactNode[] | ||
): FunctionComponentElement<P>; | ||
function createElement<P extends {}>( | ||
type: ClassType< | ||
P, | ||
ClassicComponent<P, ComponentState>, | ||
ClassicComponentClass<P> | ||
>, | ||
props?: (ClassAttributes<ClassicComponent<P, ComponentState>> & P) | null, | ||
...children: ReactNode[] | ||
): CElement<P, ClassicComponent<P, ComponentState>>; | ||
``` | ||
|
||
将 `createElement` 写两遍及以上,并配合不同的参数类型与返回值类型即可。 | ||
|
||
### 自定义类型收窄 | ||
|
||
我们可以通过 `typeof` 或 `instanceof` 做一些类型收窄工作,但有些类型甚至自定义类型的收窄判断函数需要自定义,我们可以通过 `is` 关键字定义自定义类型收窄判断函数。 | ||
|
||
问题:`isValidElement` 判断对象是否是合法的 React 元素,我们希望这个函数具备类型收窄的功能。 | ||
|
||
方案: | ||
|
||
```typescript | ||
function isValidElement<P>( | ||
object: {} | null | undefined | ||
): object is ReactElement<P>; | ||
|
||
const element: string | ReactElement = ""; | ||
|
||
if (isValidElement(element)) { | ||
element; // 自动推导类型为 ReactElement | ||
} else { | ||
element; // 自动推导类型为 string | ||
} | ||
``` | ||
|
||
基于这个方案,我们可以创建一些很有用的函数,比如 `isArray`,`isMap`,`isSet` 等等,通过 `is` 关键字时其被调用时具备类型收窄的功能。 | ||
|
||
### 用 Interface 定义函数 | ||
|
||
一般定义函数类型我们用 `type`,但有些情况下定义的函数既可被调用,也有一些默认属性值需要定义,我们可以继续用 Interface 定义。 | ||
|
||
问题:`FunctionComponent` 既可以当作函数调用,同时又能定义 `defaultProps` `displayName` 等固定属性。 | ||
|
||
方案: | ||
|
||
```typescript | ||
interface FunctionComponent<P = {}> { | ||
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null; | ||
propTypes?: WeakValidationMap<P>; | ||
contextTypes?: ValidationMap<any>; | ||
defaultProps?: Partial<P>; | ||
displayName?: string; | ||
} | ||
``` | ||
|
||
`(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null` 表示这种类型的变量可以作为函数执行: | ||
|
||
```jsx | ||
const App: FunctionComponent = () => <div />; | ||
App.displayName = "App"; | ||
``` | ||
|
||
## 3 总结 | ||
|
||
看完文章内容,相信你已经可以独立读懂 [@types/react](https://unpkg.com/browse/@types/[email protected]/index.d.ts) 这个包的所有类型定义! | ||
|
||
更多基础内容可以阅读 [精读《Typescript2.0 - 2.9》](https://github.com/dt-fe/weekly/blob/7de3c77c3bdd7304c9e4b0c0f70c3ba6968ebd29/058.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md) 与 [精读《Typescript 3.2 新特性》](https://github.com/dt-fe/weekly/blob/v2/084.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%203.2%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md),由于 TS 更新频繁,后续 TS 技巧可能继续以阅读源码方式进行,希望这次选用的 React 类型源码可以让你印象深刻。 | ||
|
||
> 讨论地址是:[精读《@types/react 值得注意的 TS 技巧》 · Issue #245 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/245) | ||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** | ||
|
||
> 关注 **前端精读微信公众号** | ||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> | ||
|
||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) |