2022年8月25日,Typescript的维护人员Daniel Rosenwasser在微软的开发博客上宣布4.8版本发布。这里对这篇博客进行引进,以便大家了解最新的动向,之所说是引进,因为我们不是直译,直译往往不符合中国人的语言习惯,会导致文章不好理解,另外我们会比博客里说得更详细,以便大家更好的理解。如果你对里面的一些内容看不太懂,又想好好研究一下,那建议你关注我们后续的Typescript全面教程。
如果你不熟悉Typescript,这里可以解释一下,它是一门在Javascript基础上创建的且具有显式类型特性的语言。这些类型是你对一个变量的期望和假设,Typescript的类型检查功能会对变量的类型进行检查。有了显式类型之后,可以避免写Javascript时的很多低级错误,例如避免把变量名称写错、访问未初始化的变量、函数的参数顺序写错等等,还可以让编辑器进行准确的智能提示,按一下键盘上的“点”,当前书写的对象的方法就都列出来了,非常方便。
那4.8都有哪些新特性呢?咱们一一列出。
交叉类型和联合类型的收窄策略优化
这个优化主要是指--strictNullChecks模式下的一些优化,提升了一致性。主要受影响的是交叉类型和联合类型的收窄操作。下面来具体说一下。
unknown的优化
先看如下代码:
function f(x: unknown, y: {} | null | undefined) {
x = y; // “{} | null | undefined”可以赋值给unknown
y = x; // 之前unknown不能赋值给“{} | null | undefined”,现在可以了
}
unknown类型虽然代表着未知类型,但是Typescript中所有的类型全列出来也就是{} | null | undefined,所以unknown和{} | null | undefined几乎等价,在4.8之前{} | null | undefined类型可以赋值给unknown类型,但是反过来则不行。现在可以了。这个可以看作是一致性提高了,虽然是不同的写法,但是只要意义相同,就可以互认。
unknown和{} | null | undefined一致后还带来了控制流分析和类型收窄方面的显著改进。所谓收窄在这个例子中就是指一个{} | null | undefined类型的变量,经过条件判断后得知了它更具体的类型,例如变成了{}类型,它的类型变得更具体了,范围变小了,这里我们叫它类型收窄。那4.8在这方面的具体改进是什么呢?看下面的例子:
{} | null | undefined可以被收窄,这个是之前版本就支持的:
function narrowUnknownishUnion(x: {} | null | undefined) {
if (x) {
x; // 收窄为{}
} else {
x; // {} | null | undefined
}
}
现在unknown也可以被收窄了,因为它是被当作{} | null | undefined看待的,这是4.8的新特性:
function narrowUnknown(x: unknown) {
if (x) {
x; // 之前是“unknown”,现在是“{}”
} else {
x; // unknown
}
}
```
#### NonNullable的优化
4.8版本中,`{}`和任何其它对象类型相交时会直接简化成该对象类型:
{} & o => o
这就意味着我们可以重写`NonNullable`:
```typescript
- type NonNullable<T> = T extends null | undefined ? never : T;
+ type NonNullable<T> = T & {};
```
改写成第二句话的形式后,很多关于`NonNullable`的操作变得更合理了,因为交叉类型可以被简化和赋值,而第一句话那种`extends`的条件类型目前还不能。所以`NonNullable<NonNullable<T>>`现在就可以被简化成`NonNullable<T>`。举例如下:
```typescript
function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {
x = y; // 之前可以
y = x; // 之前不可以,现在可以了
}
```
泛型也可以收窄了。看下面的例子,当检测到函数的参数不是`undefined`或者`null`后,泛型被收窄成`T & {}`,而这个和最新版本中的`NonNullable<T>`定义一致。
```typescript
function throwIfNullable<T>(value: T): NonNullable<T> {
if (value === undefined || value === null) {
throw Error("Nullable value!");
}
// 之前会报错,因为T不可以赋值给NonNullable<T>
// 现在收窄成了“T & {}”,成功返回,因为它就是“NonNullable<T>”
return value;
}
```
##模版字符串类型中的“infer”推导
所谓模版字符串类型就是把一个模版字符串当作类型,如下例子:
`I love ${T}`
它可以被定义为一种类型:
```typescript
type Love<T extends string> = `I love ${T}`;
type LoveYou = Love<'you'>; // 'I love you'
```
既然模版字符串也可以作为一种类型,那也就可以放到`extends`后面,并且还可以和`infer`结合使用。
看下面的例子:
```typescript
// 之前SomeNum是“number”,现在是“100”
type SomeNum = "100" extends `${infer U extends number}`? U: never;
```
可以看到模版字符串作为一种类型被放到了`extends`的后面,并且在模版字符串类型中加入了类型推导`infer`。
在之前的版本中,`U`会被推导为`number`类型,其实并不够精确,因为`extends`前面的类型比`number`更具体("100")。
4.8版本开始,`U`可以被精确地推导为100。
不过有一点需要注意一下,以下这种情况,`infer`不能做到精确推导:
```typescript
type JustNumber = "1.0" extends `${infer T extends number}` ? T : never;
```
Typescript在做类型推导时,会对字面量类型(这里是`"1.0"`)进行贪婪匹配,即尽量多的匹配,这个例子中`"1.0"`是匹配出来的`number`类型。然而虽然匹配出了`"1.0"`,但是事情还没有结束,Typescript会先把`"1.0"`转化成简单类型`number`,然后再转回字符串类型,然后检查转回的字符串是否和`"1.0"`相同,如果相同则`T`是`1.0`,如果不同则`T`是`number`,不会精确到`1.0`,用代码来概括是这样的:
```typescript
String(Number("1.0")) !== "1.0"
```
## 和对象字面量、数组字面量比较会报错
由于Javascript引用类型的变量之间比较时,并不会让各自指向的值进行比较,而是对数组的引用进行比较,举个例子:
```typescript
if(arr === []) {
// 这里不会执行到
}
```
这是初学者容易犯的一个错误,想当然的通过这种方式来判断数组是否为空。这显然是错误的做法,用等于号对数组进行比较,只能判断两个数组变量是否指向了同一个数组,而这里`[]`这种通过字面量方式创建的数组在赋值给其他变量之前肯定是一个独立的新数组,他不可能和`arr`是同一个数组。
为了避免帮助开发人员避免这种错误发生,4.8版中会对这种对字面量对象进行比较的情况直接报错,这样可以避免上述低级错误的发生。
确实是一个很有用的特性。
## 绑定模式下的类型推断
所谓绑定模式,其形式和Javascript的解构类似。
先看下面的例子:
```typescript
declare function chooseRandomly<T>(x: T, y: T): T;
let [a, b, c] = chooseRandomly([42, true, "hi!"], [0, false, "bye!"]);
// ^ ^ ^
// | | |
// | | string
// | |
// | boolean
// |
// number
```
从代码的注释可以看到,Typescript会自动推导出变量`a` `b` `c`的类型,这种类似解构的模式在Typescript中叫做绑定模式。
从`chooseRandomly`的声明来看,它的返回类型和每个参数的类型一致,而参数是`[42, true, "hi!"]` `[0, false, "bye!"]`,Typescript据此判断,T有两种类型可能`Array<number | boolean | string>`和`[number, boolean, string]`,Typescript需要决定用哪个类型,这时它将寻找其它线索来帮助判断T应该是哪个类型。这个线索就是这里使用的绑定模式`[a, b, c]`,绑定模式显然和`[number, boolean, string]`更匹配,因此`a`的类型是`number`,`b`的类型是`boolean`, `c`的类型是`string`。
截止到这里,Typescript对绑定模式的类型处理都挺合理,但是下面这个例子就有些不太合理了:
```typescript
declare function f<T>(x?: T): T;
let [x, y, z] = f();
```
由于没有参数传入,因此`T`也就没有意义,`T`没有意义则`f`的返回值也没有意义,即`f`应该没有返回值,所以绑定模式`[x, y, z]`在这里应该报错才对,但是在4.8之前,绑定模式`[x, y, z]`被作为类型推断的主要因素,因此`x` `y` `z`都被判定为`any`类型。4.8版本之后,绑定模式不再是类型推导的决定因素,只有在某些情况下需要更多的补充信息来推断具体类型时,才会参考它。因此在4.8版本中,以上绑定模式在编译时会报错。绑定类型不再直接用来进行类型推断,它只是一个辅助。
## cause的类型变为unknown
`lib.d.ts`文件有更新,其中一个显著的改变就是,`Error`中的`cause`属性的类型从`Error`变为了`unknown`。
## 没有约束的泛型不能赋值给“{}”类型
咱们先看一下下面的代码:
```typescript
function bar(value: {}) {
Object.keys(value);
}
```
根据类型定义,`value`只接受对象,看起来这段代码在Typescript的保护下,不会抛异常会正常运行。但是在4.8版本之前,却并非如此,请看如下代码:
```typescript
function bar(value: {}) {
Object.keys(value);
}
// 未约束的泛型T
function foo<T>(x: T) {
bar(x); // 之前允许,4.8会报错
}
foo(undefined);
```
`bar`声明了只接受对象,不接受`null`和`undefined`,但是4.8版本之前,它对泛型类型却未做任何限制,所以`undefined`可以借着泛型这个壳子顺利绕过`bar`的限制,造成`Object.keys(value)`报错。这显然是一个大的漏洞。4.8修复了这个问题,在4.8中,必须确保泛型是对象时才能传给`{}`类型,示例代码如下:
```typescript
function bar(value: {}) {
Object.keys(value);
}
// 被约束的泛型T
function foo<T extends {}>(x: T) {
bar(x);
}
foo(undefined); // 报错
```
`T`被限制为对象,这样`undefined`就没有办法进来搞破坏了。
## 装饰器在Typescript语法树中有变化
先看一下如下代码:
```typescript
@decorator
export class Foo {
// ...
}
```
这是之前Javascript中装饰器的语法。
前不久TC39(Ecma国际中负责ECMAScript的通用语言部分的工作组)对装饰器的设计进行了改动,其中受影响的就有上面代码的语法,新的语法是这样:
```typescript
export @decorator class Foo {
// ...
}
```
`@decorator`和`export`的位置变了。这个改动看似不大,但是对Typescript的语法树影响却不小。由于老的写法已经面世很长时间了,Typescript肯定不可能一下子抛弃老的写法,因此两种写法都需要支持。作为普通Typescript的使用者来说,4.8版本还没有支持该语法,因此不用做任何改动,新的语法会在4.9版本得到支持,而且到时候如果你还想使用老的语法,那就必须给Typescript加上“--experimentalDecorators”的标识,如果不加该标识,将只有新语法得到支持。
对于普通开发人员来说,4.8版本还是在支持老语法,但是对于需要使用Typescript编译器的API的开发人员来说,有一些变化。Typescript编译器的API的使用者主要是一些库的维护人员和Typescript的贡献者,需要使用到API的库包括ts-node、ts-loader等,另外如果你想自定义语法那也需要用到编译器API。下面咱们来说一下具体的变动。
为了能够新老语法都支持,4.8暴露了一个新的类型别名`ModifierLike`,它既可能是`Modifier`也可能是`Decorator`。
```typescript
export type ModifierLike = Modifier | Decorator;
```
`modifiers`变成了这样:
```typescript
- readonly modifiers?: NodeArray<Modifier> | undefined;
+ /**
+ * @deprecated ...
+ * Use `ts.canHaveModifiers()` to test whether a `Node` can have modifiers.
+ * Use `ts.getModifiers()` to get the modifiers of a `Node`.
+ * ...
+ */
+ readonly modifiers?: NodeArray<ModifierLike> | undefined;
```
`decorators`所有现有的属性都被标记为“deprecated”,并且是`undefined`。请参考如下代码:
```typescript
- readonly decorators?: NodeArray<Decorator> | undefined;
+ /**
+ * @deprecated ...
+ * 用`ts.canHaveDecorators()` 来检测是否一个`Node` 能有decorator.
+ * 用`ts.getDecorators()` 来获取`Node`的decorator.
+ * ...
+ */
+ readonly decorators?: undefined;
```
如果要避免这些“deprecated”的警告的话,Typescript现在暴露出四个新的函数来替代`decorators`和`modifiers`的属性。代码如下:
```typescript
function canHaveModifiers(node: Node): node is HasModifiers;
function getModifiers(node: HasModifiers): readonly Modifier[] | undefined;
function canHaveDecorators(node: Node): node is HasDecorators;
function getDecorators(node: HasDecorators): readonly Decorator[] | undefined;
```
下面是使用这些方法的例子:
```typescript
const modifiers = canHaveModifiers(myNode) ? getModifiers(myNode) : undefined;
```
需要注意的是,每次调用`getModifiers`和`getDecorators`会创建一个新的数组。
## 避免把解构当作类型定义
先看一下这段代码:
```typescript
declare function makePerson({ name: string, age: number }): Person;
```
我们理所当然的会认为函数`makePerson`内部定义的`name`和`age`这两个变量的类型分别为`string`和`number`。这个理解其实是错误的,因为`{ name: string, age: number }`这段代码作为函数参数时,它实际上是Javascript的解构重命名操作,解构重命名操作语法的优先级要高于Typescript类型定义语法,因此上面这段代码并没有对`name`和`number`进行类型定义,而是给他们分别定义了别名`string`和`number`,而这里迷惑人的地方是,`string`和`number`并不是Javascript的保留字,作为变量使用完全没有问题。误解会给我们的代码带来bug,并且会让很多人在这里浪费时间。
总结一句话就是,我们很容易错误地把解构当作类型定义。
4.8对这种情况进行了failfast的处理,就是当我们像上面这么写时,Typescript会直接指出错误,帮助开发人员尽快解决问题。
那么正确的写法是什么样的呢?像下面这样就可以了:
```typescript
declare function makePerson(options: { name: string, age: number }): Person;
// 或者
declare function makePerson({ name, age }: { name: string, age: number }): Person;
```
## Javascript文件中不再能导入导出类型
在Javascript文件中使用`--checkJs`或者`@ts-check`的情况下,Typescript之前允许Javascript文件通过`import` `export`导入导出一个没有值的纯类型。在4.8中将不再允许这样做了。这种直接访问类型的代码在ECMAScript中是不能被识别的,最优雅的方式是通过JSDoc来声明类型,JSDoc的内容都在注释中,对Javascript代码没有侵入性,即使脱离Typescript,Javascript代码也不会受影响。
以下是之前错误的使用方式:
```typescript
// @ts-check
// 在4.8中会报错,因为SomeType不是一个值,而是类型
// 如果当作纯Javascript运行,在运行时会报错
import { someValue, SomeType } from "some-module";
/**
* @type {SomeType}
*/
export const myValue = someValue;
/**
* @typedef {string | number} MyType
*/
// Will fail at runtime because 'MyType' is not a value.
export { MyType as MyExportedType };
```
正确的方式是使用JSDoc的方式导入类型。
下面的代码示范了如何导入一个类型,减号表示不对的代码,加号表示正确的代码:
```typescript
- import { someValue, SomeType } from "some-module";
+ import { someValue } from "some-module";
/**
- * @type {SomeType}
+ * @type {import("some-module").SomeType}
*/
export const myValue = someValue;
```
下面的代码示范如何导出一个类型,减号表示不对的代码,加号表示正确的代码:
```typescript
/**
* @typedef {string | number} MyType
*/
+ /**
+ * @typedef {MyType} MyExportedType
+ */
- export { MyType as MyExportedType };
```
可以看到,Javascript中不再涉及到任何类型,类型相关的代码都被放到了JSDoc中。
## 性能提升
Typescript 4.8进行了不少优化,这优化可以提升`--watch`、`--incremental`和`--build`的性能。举个例子,在`--watch`模式下,Typescript不再更新没有改动的文件的时间戳,这样可以让rebuild变得更快速,另外有些其他build工具可能会监测Typescript产出文件的变化,时间戳改变让它们也无认为需要做相应的build工作,现在这些都可以避免了。另外还有优化是可以在`--build`、`--watch`和`--incremental`之间重复使用一些信息,提升了性能。
具体会有多大的性能提升呢?基于他们内部相当大量的代码库来看,常用操作大概有10%-25%的时间节省。
## 更快地查找所有引用
当在编辑器中运行“查找所有引用”的功能时,Typescript现在可以在聚合引用时表现的更聪明一些。在Typescript自身代码库中搜索一个广泛使用的标识符时能够减少20%的时间。
## 自动导入中排除指定文件
Typescript 4.8引入了一个编辑器设置项,用来设置需要从自动导入中排除的文件。在VSCode中,文件名或者glob可以添加到“Settings”(设置)里的“Auto Import File Exclude Patterns”下,或者直接编辑`.vscode/settings.json`文件,`.vscode`目录可以存在于项目的根目录或者当前用户的根目录。
具体的JSON设置如下:
```typescript
{
// 注意,`javascript.preferences.autoImportFileExcludePatterns` 可以用来给Javascript文件使用.
"typescript.preferences.autoImportFileExcludePatterns": [
"**/node_modules/@types/node"
]
}
```
这个功能比较有用的点在于,当你确实需要某个模块参与编译,但是你又很少需要导入它。这些模块可能有很多的导出项,会沾满你的自动导入列表选项,这样你真正需要导入的模块导出项会被淹没在其中。这个功能可以帮你排除掉那些模块。
## 完结
以上就是4.8正式版的新特性。
Typescript是用来提高我们的开发效率的,但是它的特性还是挺琐碎的,目前为止也没有能快速跟进的中文文档,而且英文文档也不够聚焦。我将会提供能够帮助我们开发人员学习Typescript的文档和教程,帮助大家更轻松地跟上它的步伐。