typescript4.2 新特性

程序员成长指北

共 9946字,需浏览 20分钟

 · 2021-04-03

作者:@chao wu

原文:https://zhuanlan.zhihu.com/p/352539925

前言

2021年2月23日,微软发布了typescript4.2版本,我们来看一下有哪些新的特性

更加智能的保留类型别名

TypeScript可以使用type定义一个类型,用来标识某个变量的类型,并且可以自动推断出赋值后新变量的类型,比如以下代码:

  1. export type BasicPrimitive = number | string | boolean;


  2. export function doStuff(value: BasicPrimitive) {

  3. let x = value;

  4. return x;

  5. }

我们可以将上面的代码粘贴到TS Playground中运行,然后将鼠标hover到变量上,发现ts会自动推断出x变量的类型,如下图所示:

但是我们将代码稍做改造,如下:

  1. export type BasicPrimitive = number | string | boolean;


  2. export function doStuff(value: BasicPrimitive) {

  3. if (Math.random() < 0.5) {

  4. return undefined;

  5. }

  6. return value;

  7. }

此时你猜想一下doStuff函数的返回值的类型,是BasicPrimitive | undefined ?

结果和你想的可能不一样,如下图所示:

那为什么会这样?

好吧,这与TypeScript如何在内部表示类型有关。当你从一个或多个联合类型创建新的联合类型时,它会将这些类型转成新的扁平化联合类型,但是这样做会丢失原有的类型信息。

在TypeScript 4.2中,内部结构就变得更加智能了,你可以在 TS Playground 中切换编译版本为4.2,你会发现类型推断很完美,如下图所示:

不可跟踪的rest元素

TS中我们可以用元组类型去标识一个数组的类型,例如:

  1. let a: [string, number, boolean] = ['hello world', 10, false];

但是以上写法,元组中参数的个数是固定的,但如果number的数量是不固定的呢?

对TS熟悉的人可能会这么去写:

  1. let a: [string, ...number[], boolean] = ['hello world', 10, false];

但这在4.2以下版本,会报以下错误:

原因就是number的数量是不可预知的,你必须将它放到boolean后面,但这会和我们的代码逻辑产生冲突。而这一切在4.2中会变得很和谐:

值得注意的是,如果你使用的是4.0版本,你可以这样修改你的代码,会发现报错也会消失(但需要注意的是,4.1依然会报错)

  1. type Original = [string, ...number[]];

  2. type NewType = [...Original, boolean, undefined];


  3. let a: NewType = ['hello world', 10, false];


  4. a = ['hello world', 10, 8, 3, false];

更进一步,我们可以用这个特性去定义一个“拥有任意个前导参数,且最后是几个固定类型的参数”,比如:

  1. declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void;


  2. doStuff(/*shouldCapitalize:*/ false)

  3. doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);

虽然rest元素可以处在元组的任意位置,但唯一的限制是“每个元组仅一个rest元素,在rest元素之后不能有其他rest元素”,举个例子:

  1. interface Clown { /*...*/ }

  2. interface Joker { /*...*/ }


  3. let StealersWheel: [...Clown[], "me", ...Joker[]];

  4. // ~~~~~~~~~~ Error!

  5. // A rest element cannot follow another rest element.


  6. let StringsAndMaybeBoolean: [...string[], boolean?];

  7. // ~~~~~~~~ Error!

  8. // An optional element cannot follow a rest element.

--noPropertyAccessFromIndexSignature

有如下代码:

  1. interface Person {

  2. /** 姓名 */

  3. name: string;

  4. /** 通过索引签名去扩充数据字段 */

  5. [x: string]: any;

  6. }


  7. function processOptions(person: Person) {

  8. console.log(`name: ${person.name}, age: ${person.age}`);

  9. }


  10. processOptions({ name: 'jack', age: 22 } as Person);

首先以上代码不会报错。

在代码中,age来自于索引签名,但往往为了区别于已知字段(比如name),用户可能会想让编译器报错,这时你可以在tsconfig.json中设置:

  1. "noPropertyAccessFromIndexSignature": true,

然后重新启动前端项目,即可发现报错

  1. Property 'age' comes from an index signature, so it must be accessed with ['age'].

抽象构造签名

有如下代码:

  1. interface IShape {

  2. getArea(): number;

  3. }


  4. class Shape implements IShape {

  5. getArea() {

  6. return 2;

  7. }

  8. }


  9. function makeSubclassWithArea(Ctor: new () => IShape) {

  10. return class extends Ctor {}

  11. }


  12. let MyShape = makeSubclassWithArea(Shape);


  13. const a = new MyShape();

  14. console.log(a.getArea()); // 2

上述代码功能很简单:

创建子类构造器,比如MyShape通过继承Shape构造器来创建。但是我们想通过抽象类来实现的话,代码可能会变成这样:

  1. abstract class Shape {

  2. // 在子类中去实现该方法

  3. abstract getArea(): number;

  4. }


  5. interface IShape {

  6. getArea(): number;

  7. }


  8. function makeSubclassWithArea(Ctor: new () => IShape) {

  9. return class extends Ctor {

  10. // 实现抽象类中的抽象函数

  11. getArea() {

  12. return 2;

  13. }

  14. }

  15. }


  16. let MyShape = makeSubclassWithArea(Shape);

但是遗憾的是,编译器会报错:

另外,如果使用InstanceType也会报同样的错:

这就是为什么TypeScript 4.2允许您在构造函数签名上指定抽象修饰符。以下是处理方案:

  1. abstract class Shape {

  2. // 在子类中去实现该方法

  3. abstract getArea(): number;

  4. }


  5. type AbstractConstructor<T> = abstract new (...args: any[]) => T;


  6. function makeSubclassWithArea<T extends AbstractConstructor<object>>(Ctor: T) {

  7. abstract class SubClass extends Ctor {

  8. // 实现抽象类中的抽象函数

  9. getArea() {

  10. return 2;

  11. }

  12. }

  13. return SubClass;

  14. }


  15. class SubclassWithArea extends makeSubclassWithArea(Shape) {

  16. customMethod() {

  17. return this.getArea();

  18. }

  19. }


  20. const a = new SubclassWithArea();

  21. console.log(a.getArea()); // 2

  22. console.log('customMethod result:' + a.customMethod()); // customMethod result: 2

使用--explainFiles了解您的项目结构

使用以下指令时,TypeScript编译器将给出一些非常长的输出,关于import信息。

  1. tsc --explainFiles


  2. # 如果全局没安装typescript,使用以下命令

  3. # npx tsc --explainFiles

信息如下(提取自命令行工具):

  1. ...

  2. node_modules/typescript/lib/lib.es5.d.ts

  3. Library referenced via 'es5' from file 'node_modules/typescript/lib/lib.es2015.d.ts'

  4. Library referenced via 'es5' from file 'node_modules/typescript/lib/lib.es2015.d.ts'

  5. node_modules/@types/react/jsx-runtime.d.ts

  6. Imported via "react/jsx-runtime" from file 'src/index.tsx' with packageId '@types/react/jsx-runtime.d.ts@17.0.2' to import 'jsx' and 'jsxs' factory functions

  7. node_modules/@types/react/index.d.ts

  8. Imported via 'react' from file 'src/framework/lib/BuryingPoint.tsx' with packageId '@types/react/index.d.ts@17.0.2'

  9. Imported via 'react' from file 'node_modules/antd-mobile/lib/accordion/index.d.ts' with packageId '@types/react/index.d.ts@17.0.2'

  10. Imported via 'react' from file 'node_modules/antd-mobile/lib/action-sheet/index.d.ts' with packageId '@types/react/index.d.ts@17.0.2'

  11. node_modules/@types/react-router/index.d.ts

  12. Imported via 'react-router' from file 'src/framework/routes/route.tsx' with packageId '@types/react-router/index.d.ts@5.1.11'

  13. ...

如果你觉得命令行工具中看的不舒服,可以将信息提取到txt或者vscode中

  1. # 提前到txt

  2. npx tsc --explainFiles > expanation.txt


  3. # 提前到vscode

  4. npx tsc --explainFiles | code -

改进逻辑表达式中的未调用函数检查

TypeScript的未调用函数检查现在适用于&&和||表达式。

在strictNullChecks: true下,以下代码现在将会报错。

  1. function shouldDisplayElement(element: Element) {

  2. // ...

  3. return true;

  4. }


  5. function getVisibleItems(elements: Element[]) {

  6. return elements.filter(e => shouldDisplayElement && e.children.length)

  7. // ~~~~~~~~~~~~~~~~~~~~

  8. // This condition will always return true since the function is always defined.

  9. // Did you mean to call it instead.

  10. }

解构变量可以明确标记为未使用

  1. # 首先在tsconfig.json中配置noUnusedLocals为true

  2. "noUnusedLocals": true,

以下代码中,_a未被使用(4.2以下版本会报以下错误)

  1. const [_a, b] = [12, 3];


  2. console.log(b); // TS6133: '_a' is declared but its value is never read.

你可能想要的是:告诉TS,以下划线开头的变量表示未使用变量,只负责占位,请不要报错。

此时,你只需要将ts版本升级为4.2即可(这确实是一个很重要的更新)。

但值得注意的是,以下代码,依然会报错:

  1. const [a, b] = [12, 3];


  2. console.log(b); // TS6133: 'a' is declared but its value is never read.

可选属性和字符串索引签名之间的宽松规则

先看一段代码(运行环境: < TS4.2),会报错:

  1. type WesAndersonWatchCount = {

  2. "Fantastic Mr. Fox"?: number;

  3. "The Royal Tenenbaums"?: number;

  4. "Moonrise Kingdom"?: number;

  5. "The Grand Budapest Hotel"?: number;

  6. };


  7. // same as

  8. // type WesAndersonWatchCount = {

  9. // "Fantastic Mr. Fox"?: number | undefined;

  10. // "The Royal Tenenbaums"?: number | undefined;

  11. // "Moonrise Kingdom"?: number | undefined;

  12. // "The Grand Budapest Hotel"?: number | undefined;

  13. // }


  14. declare const wesAndersonWatchCount: WesAndersonWatchCount;

  15. const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;

  16. // ~~~~~~~~~~~~~~~ error!

  17. // Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.

  18. // Property '"Fantastic Mr. Fox"' is incompatible with index signature.

  19. // Type 'number | undefined' is not assignable to type 'number'.

  20. // Type 'undefined' is not assignable to type 'number'. (2322)

然而上面的代码在4.2中是可以通过编译的,但是改造一下:

  1. type WesAndersonWatchCount = { // 删除问号

  2. "Fantastic Mr. Fox": number | undefined;

  3. "The Royal Tenenbaums": number | undefined;

  4. "Moonrise Kingdom": number | undefined;

  5. "The Grand Budapest Hotel": number | undefined;

  6. }


  7. declare const wesAndersonWatchCount: WesAndersonWatchCount;

  8. const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;


  9. // ~~~~~~~~~~~~~~~ error!

  10. // Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.

  11. // Property '"Fantastic Mr. Fox"' is incompatible with index signature.

  12. // Type 'number | undefined' is not assignable to type 'number'.

  13. // Type 'undefined' is not assignable to type 'number'.(2322)


  14. // 以下场景在TypeScript 4.2也会报错

  15. // Type 'undefined' is not assignable to type 'number'

  16. movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;

。。。其他变化。。。

lib.d.ts 的更新

noImplicitAny错误适用于宽松的yeild表达式:

  1. # 首先设置noImplicitAny为true

  2. "noImplicitAny": true

然后在4.2中运行以下代码:

  1. function* g1() {

  2. const value = yield 1;

  3. // ~~~~~~~

  4. // Error!

  5. // 'yield' expression implicitly results in an 'any' type

  6. // because its containing generator lacks a return-type annotation.

  7. }


  8. function* g2() {

  9. // No error.

  10. // The result of `yield 1` is unused.

  11. yield 1;

  12. }


  13. function* g3() {

  14. // No error.

  15. // `yield 1` is contextually typed by 'string'.

  16. const value: string = yield 1;

  17. }


  18. function* g4(): Generator<number, void, string> {

  19. // No error.

  20. // TypeScript can figure out the type of `yield 1`

  21. // from the explicit return type of `g3`.

  22. const value = yield 1;

  23. }

然而以上代码中g1方法在4.2以下版本不会报错。

in运算符不再允许在右侧使用基本类型

  1. // [< 4.2] The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.

  2. "foo" in 42


  3. // [= 4.2] The right-hand side of an 'in' expression must not be a primitive.

  4. "foo" in 42

元组展开限制

TypeScript中可以使用扩展语法(...)来创建新的元组类型。

  1. type NumStr = [number, string];

  2. type NumStrNumStr = [...NumStr, ...NumStr];

但有时,这些元组类型可能会意外增长为巨大的类型,这可能会使类型检查花费很长时间。在4.2版本后,TypeScript设置了限制器以避免执行所有工作。

.d.ts扩展 不能在导入路径中使用

在TypeScript 4.2中,导入路径中包含.d.ts现在是错误的。

  1. // must be changed something like

  2. // - "./foo"

  3. // - "./foo.js"

  4. import { Foo } from "./foo.d.ts";

恢复模板字面量推断

  1. declare const yourName: string;


  2. const bar = `hello ${yourName}`;


  3. type C = typeof bar;

下面分别展示的是4.1 和 4.2的不同:

ts 4.1

ts 4.2

但是如果你只想让此特性生效一次,你可以这样改造:

  1. declare const yourName: string;


  2. const bar = `hello ${yourName}` as const;

  3. const baz = `hello ${yourName}`;


  4. type C = typeof bar; // C -> `hello ${string}`


  5. type D = typeof baz; // D -> string

值得一提的是4.1版本(4.1以下会报错),以下代码效果和4.2一致:

  1. declare const yourName: string;


  2. const bar = `hello ${yourName}` as const;


  3. type C = typeof bar; // C -> `hello ${string}`

❤️爱心三连击

1.看到这里了就点个在看支持下吧,你的点赞在看是我创作的动力。

2.关注公众号程序员成长指北,回复「1」加入高级前端交流群!「在这里有好多 前端 开发者,会讨论 前端 Node 知识,互相学习」!

3.也可添加微信【ikoala520】,一起成长。

“在看转发”是最大的支持

浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报