Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c542af0ab | ||
|
|
4b3aae675d | ||
|
|
532d3bd6f0 | ||
|
|
cb20d65c2c | ||
|
|
07b6017451 | ||
|
|
2c5f8ca4f3 | ||
|
|
cd543b1fd4 | ||
|
|
88560b6626 | ||
|
|
1cf5e339c9 |
@@ -6,7 +6,7 @@
|
||||
|
||||
前端界的好文精读,每周更新!
|
||||
|
||||
最新精读:<a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
|
||||
最新精读:<a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
|
||||
|
||||
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
|
||||
|
||||
@@ -164,6 +164,8 @@
|
||||
- <a href="./前沿技术/204.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%BB%98%E8%AE%A4%E3%80%81%E5%91%BD%E5%90%8D%E5%AF%BC%E5%87%BA%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">204.精读《默认、命名导出的区别》</a>
|
||||
- <a href="./前沿技术/205.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20with%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">205.精读《JS with 语法》</a>
|
||||
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
|
||||
- <a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
|
||||
- <a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
|
||||
|
||||
### 设计模式
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
Infer 关键字用于条件中的类型推导。
|
||||
|
||||
Typescript 官网也拿 `ReturnType` 这一经典例子说明它的作用:
|
||||
|
||||
```typescript
|
||||
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
|
||||
```
|
||||
|
||||
理解为:如果 `T` 继承了 `(...args: any[]) => any` 类型,则返回类型 `R`,否则返回 `any`。其中 `R` 是什么呢?`R` 被定义在 `extends (...args: any[]) => infer R` 中,即 R 是从传入参数类型中推导出来的。
|
||||
|
||||
## 精读
|
||||
|
||||
我们可以从两个视角来理解 `infer`,分别是需求角度与设计角度。
|
||||
|
||||
### 需求角度理解 infer
|
||||
|
||||
实现 `infer` 这个关键字一定是背后存在需求,这个需求是普通 Typescript 能力无法满足的。
|
||||
|
||||
设想这样一个场景:实现一个函数,接收一个数组,返回第一项。
|
||||
|
||||
我们无法用泛型来描述这种类型推导,因为泛型类型是一个整体,而我们想要返回的是入参其中某一项,我们并不能通过类似 `T[0]` 的写法拿到第一项类型:
|
||||
|
||||
```typescript
|
||||
function xxx<T>(...args: T[]): T[0]
|
||||
```
|
||||
|
||||
而实际上不支持这种写法也是合理的,因为这次是获取第一项类型,如果 `T` 是一个对象,我们想返回其中 `onChange` 这个 Key 的返回值类型,就不知道如何书写了。所以此时必须用一种新的语法实现,就是 `infer`。
|
||||
|
||||
### 设计角度理解 infer
|
||||
|
||||
从类型推导功能来看,泛型功能非常强大,我们可以用泛型描述调用时才传入的类型,并提前将它描述在类型表达式中:
|
||||
|
||||
```typescript
|
||||
function xxx<T>(value: T): { result: T }
|
||||
```
|
||||
|
||||
但我们发现 `T` 这个泛型太整体化了,我们还不具备从中 Pick 子类型的能力。也就是对于 `xxx<{label: string}>` 这个场景,`T = {label: string}`,但我们无法将 `R` 定义为 `{label: R}` 这个位置,因为泛型是一个不可拆分的整体。
|
||||
|
||||
而且实际上为了类型安全,我们也不能允许用户描述任意的类型位置,**万一传入的类型结构不是 `{label: xxx}` 而是一个回调 `() => void`,那子类型推导岂不是建立在了错误的环境中。** 所以考虑到想要拿到 `{label: infer R}`,首先参数必须具备 `{label: xxx}` 的结构,所以正好可以将 `infer` 与条件判断 `T extends xxx ? A : B` 结合起来用,即:
|
||||
|
||||
```typescript
|
||||
type GetLabelTypeFromObject<T> = T extends { label: infer R } ? R : never
|
||||
|
||||
type Result = GetLabelTypeFromObject<{ label: string }>;
|
||||
// type Result = string
|
||||
```
|
||||
|
||||
即如果 `T` 遵循 `{ label: any }` 这样一个结构,那么我可以将这个结构中任何变量位置替换为 `infer xxx`,如果传入类型满足这个结构(TS 静态解析环节判断),则可以基于这个结构体继续推导,所以在推导过程中我们就可以使用 `infer xxx` 推断的变量类型。
|
||||
|
||||
回过头来看第一个需求,拿到第一个参数类型就可以用 `infer` 实现了:
|
||||
|
||||
```typescript
|
||||
type GetFirstParamType<T> = T extends (...args: infer R) => any ? R[0] : never
|
||||
```
|
||||
|
||||
可以理解为,如果此时 `T` 满足 `(...args: any) => any` 这个结构,同时我们用 `infer R` 表示 `R` 这个临时变量指代第一个 `any` 运行时类型,那么整个函数返回的类型就是 `R`。如果 `T` 都不满足 `(...args: any) => any` 这个结构,比如 `GetFirstParamType<number>`,那这种推导根本无从谈起,直接返回 `never` 类型兜底,当然也可以自定义比如 `any` 之类的任何类型。
|
||||
|
||||
## 概述
|
||||
|
||||
我们理解了 `infer` 含义后,再结合 [conditional infer](https://learntypescript.dev/09/l2-conditional-infer) 这篇文章理解里面的例子,有助于加深记忆。
|
||||
|
||||
```typescript
|
||||
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
|
||||
// type of item1 is `number`
|
||||
type item1 = ArrayElementType<number[]>;
|
||||
// type of item1 is `{name: string}`
|
||||
type item2 = ArrayElementType<{ name: string }>;
|
||||
```
|
||||
|
||||
可以看到,`ArrayElementType` 利用了条件推断与 `infer`,表示了这样一个逻辑:如果 `T` 类型是一个数组,且我们将数组的每一项定义为 `E` 类型,那么返回类型就为 `E`,否则为 `T` 整体类型本身。
|
||||
|
||||
所以对于 `item1` 是满足结构的,所以返回 `number`,而 `item2` 不满足结构,所以返回其类型本身。
|
||||
|
||||
特别补充一点,对于下面的例子返回什么呢?
|
||||
|
||||
```typescript
|
||||
type item3 = ArrayElementType<[number, string]>;
|
||||
```
|
||||
|
||||
答案是 `number | string`,原因是我们用多个 `infer E`(`(infer E)[]` 相当于 `[infer E, infer E]...` 不就是多个变量指向同一个类型代词 `E` 嘛)同时接收到了 `number` 和 `string`,所以可以理解为 `E` 时而为 `number` 时而为 `string`,所以是或关系,这就是协变。
|
||||
|
||||
那如果是函数参数呢?
|
||||
|
||||
```typescript
|
||||
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
|
||||
? U : never
|
||||
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
|
||||
```
|
||||
|
||||
发现结果是 `string & number`,也就是逆变。但这个例子也是同一个 `U` 时而为 `string` 时而为 `number` 呀,为什么是且的关系,而不是或呢?
|
||||
|
||||
其实协变或逆变与 `infer` 参数位置有关。在 TypeScript 中,对象、类、数组和函数的返回值类型都是协变关系,而函数的参数类型是逆变关系,所以 `infer` 位置如果在函数参数上,就会遵循逆变原则。
|
||||
|
||||
> 逆变与协变:
|
||||
>
|
||||
> - 协变(co-variant):类型收敛。
|
||||
> - 逆变(contra-variant):类型发散。
|
||||
|
||||
关于逆变与协变更深入的话题可以再开一篇文章了,这里就不细讲了,对于 `infer` 理解到这里就够啦。
|
||||
|
||||
## 总结
|
||||
|
||||
`infer` 关键字让我们拥有深入展开泛型的结构,并 Pick 出其中任何位置的类型,并作为临时变量用于最终返回类型的能力。
|
||||
|
||||
对于 Typescript 类型编程,最大的问题莫过于希望实现一个效果却不知道用什么语法,`infer` 作为一个强大的类型推导关键字,势必会在大部分复杂类型推导场景下派上用场,所以在遇到困难时,可以想想是不是能用 `infer` 解决问题。
|
||||
|
||||
> 讨论地址是:[精读《Typescript infer 关键字》· Issue #346 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/346)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
@@ -0,0 +1,275 @@
|
||||
Typescript 4.4 正式发布了!距离 Typescript 4.5 发布还有三个月的时间,抓紧上车学习吧!
|
||||
|
||||
本周精读的文章:[announcing-typescript-4-4](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/)
|
||||
|
||||
## 概述
|
||||
|
||||
### 更智能的自动类型收窄
|
||||
|
||||
类型收窄功能非常方便,它可以让 Typescript 尽可能的像 Js 一样自动智能判定类型,从而避免类型定义的工作,让你的 Typescript 写得更像 Js。
|
||||
|
||||
其实这个功能早就有了,在我们 [精读《Typescript2.0 - 2.9》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/58.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md#%E8%87%AA%E5%8A%A8%E7%B1%BB%E5%9E%8B%E6%8E%A8%E5%AF%BC) 就已经介绍过,当时用的名词是自动类型推导,这次用了更精确的自动类型收窄一词,因为只有类型收窄是安全的,比如:
|
||||
|
||||
```typescript
|
||||
function foo(arg: unknown) {
|
||||
if (typeof arg === "string") {
|
||||
// We know 'arg' is a string now.
|
||||
console.log(arg.toUpperCase());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
而在 Typescript 4.4 之前的版本,如果我们将这个判定赋值给一个变量,再用到 `if` 分支里,就无法正常收窄类型了:
|
||||
|
||||
```typescript
|
||||
function foo(arg: unknown) {
|
||||
const argIsString = typeof arg === "string";
|
||||
if (argIsString) {
|
||||
console.log(arg.toUpperCase());
|
||||
// ~~~~~~~~~~~
|
||||
// Error! Property 'toUpperCase' does not exist on type 'unknown'.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个问题在 Typescript 4.4 得到了解决,实际上是把这种类型收窄判断逻辑加深了,即无论这个判断写在哪都可以生效。所以下面这种解构的用法判断也可以推断出类型收窄:
|
||||
|
||||
```typescript
|
||||
type Shape =
|
||||
| { kind: "circle", radius: number }
|
||||
| { kind: "square", sideLength: number };
|
||||
|
||||
function area(shape: Shape): number {
|
||||
// Extract out the 'kind' field first.
|
||||
const { kind } = shape;
|
||||
|
||||
if (kind === "circle") {
|
||||
// We know we have a circle here!
|
||||
return Math.PI * shape.radius ** 2;
|
||||
}
|
||||
else {
|
||||
// We know we're left with a square here!
|
||||
return shape.sideLength ** 2;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
不仅是单一的判断,Typescript 4.4 还支持复合类型推导:
|
||||
|
||||
```typescript
|
||||
function doSomeChecks(
|
||||
inputA: string | undefined,
|
||||
inputB: string | undefined,
|
||||
shouldDoExtraWork: boolean,
|
||||
) {
|
||||
const mustDoWork = inputA && inputB && shouldDoExtraWork;
|
||||
if (mustDoWork) {
|
||||
// We can access 'string' properties on both 'inputA' and 'inputB'!
|
||||
const upperA = inputA.toUpperCase();
|
||||
const upperB = inputB.toUpperCase();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`mustDoWork` 为 `true` 的分支就意味着 `inputA`、`inputB` 均收窄为 `string` 类型。
|
||||
|
||||
这种深层的判定还体现在,一个具备类型判断的变量进行再计算,生成的变量还具有类型判断功能:
|
||||
|
||||
```typescript
|
||||
function f(x: string | number | boolean) {
|
||||
const isString = typeof x === "string";
|
||||
const isNumber = typeof x === "number";
|
||||
const isStringOrNumber = isString || isNumber;
|
||||
if (isStringOrNumber) {
|
||||
x; // Type of 'x' is 'string | number'.
|
||||
}
|
||||
else {
|
||||
x; // Type of 'x' is 'boolean'.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,我们几乎可以像写 Js 一样写 Typescript,4.4 支持了大部分符合直觉的推导非常方便。但要注意的是,Typescript
|
||||
毕竟不是运行时,无法做到更彻底的自动推断,但足以支持绝大部分场景。
|
||||
|
||||
### 下标支持 Symbol 与模版字符串类型判定
|
||||
|
||||
原本我们定义一个用下标访问的对象是这样的:
|
||||
|
||||
```typescript
|
||||
interface Values {
|
||||
[key: string]: number
|
||||
}
|
||||
```
|
||||
|
||||
现在也支持 Symbol 拉:
|
||||
|
||||
```typescript
|
||||
interface Colors {
|
||||
[sym: symbol]: number;
|
||||
}
|
||||
|
||||
const red = Symbol("red");
|
||||
const green = Symbol("green");
|
||||
const blue = Symbol("blue");
|
||||
|
||||
let colors: Colors = {};
|
||||
|
||||
colors[red] = 255; // Assignment of a number is allowed
|
||||
let redVal = colors[red]; // 'redVal' has the type 'number'
|
||||
|
||||
colors[blue] = "da ba dee"; // Error: Type 'string' is not assignable to type 'number'.
|
||||
```
|
||||
|
||||
而且对于特定的字符串模版也支持类型匹配,比如希望以 `data-` 开头的下标是一种独立类型,可以这么定义:
|
||||
|
||||
```typescript
|
||||
interface Options {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
let a: Options = {
|
||||
width: 100,
|
||||
height: 100,
|
||||
"data-blah": true, // Error! 'data-blah' wasn't declared in 'Options'.
|
||||
};
|
||||
|
||||
interface OptionsWithDataProps extends Options {
|
||||
// Permit any property starting with 'data-'.
|
||||
[optName: `data-${string}`]: unknown;
|
||||
}
|
||||
|
||||
let b: OptionsWithDataProps = {
|
||||
width: 100,
|
||||
height: 100,
|
||||
"data-blah": true, // Works!
|
||||
|
||||
"unknown-property": true, // Error! 'unknown-property' wasn't declared in 'OptionsWithDataProps'.
|
||||
};
|
||||
```
|
||||
|
||||
这个对于 HTML 的 `data-` 属性非常有帮助。
|
||||
|
||||
同时还支持联合类型定义,下面两种类型定义方式是等价的:
|
||||
|
||||
```typescript
|
||||
interface Data {
|
||||
[optName: string | symbol]: any;
|
||||
}
|
||||
|
||||
// Equivalent to
|
||||
|
||||
interface Data {
|
||||
[optName: string]: any;
|
||||
[optName: symbol]: any;
|
||||
}
|
||||
```
|
||||
|
||||
### 更严格的错误捕获类型
|
||||
|
||||
在 `unknown` 类型出来之前,Typescript 以 `any` 作为抛出错误的默认类型,毕竟谁也不知道抛出错误的类型是什么:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
// Who knows what this might throw...
|
||||
executeSomeThirdPartyCode();
|
||||
}
|
||||
catch (err) { // err: any
|
||||
console.error(err.message); // Allowed, because 'any'
|
||||
err.thisWillProbablyFail(); // Allowed, because 'any' :(
|
||||
}
|
||||
```
|
||||
|
||||
Who knows what this might throw... 这句话很有意思,一个函数任何地方都可能出现运行时错误,这根本不是静态分析可以解决的,所以不可能自动推断错误类型,所以只能用 `any`。
|
||||
|
||||
在 Typescript 4.4 的 `--useUnknownInCatchVariables` 或 `--strict` 模式下都将以 `unknown` 作为捕获到错误的默认类型。
|
||||
|
||||
相比不存在的类型 `never`,`unknown` 仅仅是不知道是什么类型而已,所以不能像 `any` 一样当作任何类型使用,但我们可以将其随意推断为任意类型:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
executeSomeThirdPartyCode();
|
||||
}
|
||||
catch (err) { // err: unknown
|
||||
// Error! Property 'message' does not exist on type 'unknown'.
|
||||
console.error(err.message);
|
||||
|
||||
// Works! We can narrow 'err' from 'unknown' to 'Error'.
|
||||
if (err instanceof Error) {
|
||||
console.error(err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果觉得这样做麻烦,也可以重新申明类型为 `any`:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
executeSomeThirdPartyCode();
|
||||
}
|
||||
catch (err: any) {
|
||||
console.error(err.message); // Works again!
|
||||
}
|
||||
```
|
||||
|
||||
但这样做其实并不合适,因为即便是考虑了运行时因素,理论上还是可能发生意外错误,所以对错误过于自信的类型推断是不太合适的,最好保持其 `unknown` 类型,对所有可能的边界情况做处理。
|
||||
|
||||
### 明确的可选属性
|
||||
|
||||
对象的可选属性在类型描述时有个含糊不清的地方,比如:
|
||||
|
||||
```typescript
|
||||
interface Person {
|
||||
name: string,
|
||||
age?: number;
|
||||
}
|
||||
```
|
||||
|
||||
其实 Typescript 对其的类型定义的是:
|
||||
|
||||
```typescript
|
||||
interface Person {
|
||||
name: string,
|
||||
age?: number | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
为什么要这么定义呢?因为很多情况下,没有这个 key,与这个 key 的值为 `undefined` 的表现是等价的。但比如 `Object.keys` 场景下这两种表现却又不等价,所以理论上对于 `age?: number` 的确切表述是:要么没有 `age`,要么有 `age` 且类型为 `number`,也就是说下面的写法应该是错误的:
|
||||
|
||||
```typescript
|
||||
// With 'exactOptionalPropertyTypes' on:
|
||||
const p: Person = {
|
||||
name: "Daniel",
|
||||
age: undefined, // Error! undefined isn't a number
|
||||
};
|
||||
```
|
||||
|
||||
在 Typescript 4.4 中同时开启 `--exactOptionalPropertyTypes` 与 `--strictNullChecks` 即可生效。
|
||||
|
||||
仔细想想这是合理的,既然定义的类型不是 `undefined`,就算对象是可选类型,也不能认为赋值 `undefined` 是合理的,因为 `age?: number` 的心理预期是,要么没有这个 key,要么有但是类型为 `number`,所以当 `Object.keys` 发现 `age` 这个 key 时,值就应该是 `number`。
|
||||
|
||||
### 支持 Static Block
|
||||
|
||||
Typescript 4.4 支持了 [class static blocks](https://github.com/tc39/proposal-class-static-block#ecmascript-class-static-initialization-blocks),并且在代码块作用域内可以访问私有变量。
|
||||
|
||||
|
||||
还有一些性能提升与体验优化杂项就不一一列举了,感兴趣可以直接看原文档:[perf-improvements](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/#perf-improvements)。
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
从 Typescript 4.4 特性可以看出,Typescript 正在往 “更具备原生 JS 亲和性” 方向作出努力,这无疑会使 Typescript 变得越来越好用。
|
||||
|
||||
对更多新特性感兴趣,可以 [查看 Typescript 4.5 版本发布计划](https://github.com/microsoft/TypeScript/issues/45418)。
|
||||
|
||||
> 讨论地址是:[精读《Typescript 4.4》· Issue #348 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/348)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
@@ -78,7 +78,7 @@ ES 模块需要借助模块加载器来实现这三步。加载器在不同的
|
||||
|
||||
这就意味着我们必须一层一层的遍历文件树,转化文件并找出依赖,最后查找并且加载这些依赖。如果主线程正在等待去下载这些文件,那么很多的任务会堆积在队列中。这是因为浏览器环境下下载用了很长时间。
|
||||
|
||||
阻塞主线程会导致应用所需的模块变得很慢。将构建过程分片进行实现了在全部下载前进行获取和构建。这种查分构建的方式是 ES 模块和 CJS 模块最本质的不同。
|
||||
阻塞主线程会导致应用所需的模块变得很慢。将构建过程分片进行实现了在全部下载前进行获取和构建。这种差分构建的方式是 ES 模块和 CJS 模块最本质的不同。
|
||||
|
||||

|
||||
|
||||
|
||||
Reference in New Issue
Block a user