TypeScript 4.2 Beta版本发布:带来诸多更新,营造更好的开发体验

共 14275字,需浏览 29分钟

 ·

2021-01-18 18:52

作者 | Daniel Rosenwasser
译者 | 王强
策划 | 李俊辰
1 月 12 日,微软发布了 TypeScript 4.2 Beta 版本,本文是新版的更新内容介绍。

要安装这个 beta 版本,一种方法是使用 NuGet 获取:

https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild

也可以输入以下 npm 命令:
npm install typescript@beta

可以通过以下方式获取编辑器支持:

  • 下载 Visual Studio 2019/2017;

  • 遵循 Visual Studio Code 和 Sublime Text 的指南。

下面就来看看 TypeScript 4.2 带来了哪些新内容。

元组类型的 Rest 元素可放置于元组中的任何位置
在 TypeScript 中,元组类型用于建模具有特定长度和元素类型的数组。
// A tuple that stores a pair of numbers
let a: [number, number] = [1, 2];
// A tuple that stores a string, a number, and a boolean
let b: [string, number, boolean] = ["hello", 42, true];
长久以来,TypeScript 的元组类型变得越来越复杂,因为它们还被用来建模 JavaScript 中的参数列表之类的事物。如今,它们可以有可选元素和 rest 元素,甚至可以有用于工具链和可读性的标签。
// A tuple that has either one or two strings.
let c: [string, string?] = ["hello"];
c = ["hello", "world"];
// A labeled tuple that has either one or two strings.
let d: [first: string, second?: string] = ["hello"];
d = ["hello", "world"];
// A tuple with a *rest element* - holds at least 2 strings at the front,
// and any number of booleans at the back.
let e: [string, string, ...boolean[]];
e = ["hello", "world"];
e = ["hello", "world", false];
e = ["hello", "world", true, false, true];
在 TypeScript 4.2 中,rest 元素的使用方法得到了专门扩展。在以前的版本中,TypeScript 仅允许...rest 元素位于元组类型的最后一个位置。但现在,rest 元素可以在元组中的任何位置出现——只不过有一点限制。
let foo: [...string[], number];
foo = [123];
foo = ["hello", 123];
foo = ["hello!", "hello!", "hello!", 123];
let bar: [boolean, ...string[], boolean];
bar = [true, false];
bar = [true, "some text", false];
bar = [true, "some", "separated", "text", false];
唯一的限制是,rest 元素可以放置在元组中的任何位置,只要后面没有其他可选元素或 rest 元素即可。换句话说,每个元组仅一个 rest 元素,rest 元素之后没有可选元素。
interface Clown { /*...*/ }
interface Joker { /*...*/ }
let StealersWheel: [...Clown[], "me", ...Joker[]];
// ~~~~~~~~~~ Error!
// A rest element cannot follow another rest element.
let StringssAndMaybeBoolean: [...string[], boolean?];
// ~~~~~~~~ Error!
// An optional element cannot follow a rest element.
这些无尾随的 rest 元素可用于建模采用任意数量前置参数,后跟一些固定参数的函数。
declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void;
doStuff(/*shouldCapitalize:*/ false)
doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);

虽然 JavaScript 没有任何语法来建模前置 rest 参数,但我们可以声明...args rest 参数和一个使用前置 rest 元素的元组类型,来将 doStuff 声明为采用前置参数的函数。这样就可以建模许多现有的 JavaScript 代码了!

详细信息请参见原始拉取请求:

https://github.com/microsoft/TypeScript/pull/41544

更智能的类型别名保护
TypeScript 一直使用一组启发式方法来判断何时以及如何显示类型别名。但这些方法在复杂场景中经常会出问题。考虑以下代码段。
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
    let x = value;
    return x;
}

如果在 Visual Studio、Visual Studio Code 或 TypeScript Playground 等编辑器中将鼠标悬停在 x 上,我们将看到一个快速信息面板,其中显示了 BasicPrimitive 类型。同样,如果我们获得此文件的声明文件输出(.d.ts 输出),TypeScript 会告诉你 doStuff 返回 BasicPrimitive。

但是,如果我们返回一个 BasicPrimitive 或 undefined 会发生什么?
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
    if (Math.random() < 0.5) {
        return undefined;
    }
    return value;
}

以 TypeScript Playground 为例。虽然我们可能希望 TypeScript 将 doStuff 的返回类型显示为 BasicPrimitive | undefined,但它实际显示的是 string | number | boolean | undefined!什么鬼?

其实这和 TypeScript 在内部表示类型的方法有关。从一个或多个联合类型创建一个联合类型时,它总会将这些类型规范化为一个新的展平联合类型——但这会丢失信息。类型检查器是没办法知道 string | number | boolean 来自哪里的。

在 TypeScript 4.2 中,我们的内部结构更加智能了。在规范化类型之前,我们会保留其原始结构的某些部分来跟踪类型的构造方式。我们还将跟踪并区分类型别名和其他别名实例!

现在系统能够根据你在代码中的使用方式来打印出这些类型,这意味着作为 TypeScript 用户,你可以避免显示一些烦人的巨大类型,而这往往会转化为更好的.d.ts 文件输出、错误消息和快速信息及签名帮助中的编辑器内类型显示。

有关更多信息,请查看拉取请求:

https://github.com/microsoft/TypeScript/pull/42149

https://github.com/microsoft/TypeScript/pull/42284

模板字面量表达式具有模板字面量类型
在 TypeScript 4.1 中我们引入了一种新的类型:模板字面量类型。这些类型能够建模字符串的特定模式。
type GreetingStart = "hello" | "hi" | "sup";
declare function greet(str: `${GreetingStart} ${string}`): void;
// Works.
greet("hello world!");
// Works.
greet("hi everybody!");
// Error!
// Doesn't work with any of the patterns:
// `hello ${string}` | `hi ${string}` | `sup ${string}`
greet("hallo yes hi sup");
但在 4.1 中,模板字符串类型和模板字符串表达式之间存在一些奇怪的不一致之处。
function doStuff(str: string) {
    // Error!
    // Type 'string' is not assignable to type '`hello ${string}`'.
    let x: `hello ${string}` = `hello ${str}`
}

这是因为带有替换插槽 ${likeThis}的模板字符串表达式总是只有 string 类型。于是它们可能与我们新的模板字符串类型不兼容。

在 TypeScript 4.2 中,模板字符串表达式现在总是以模板字面量类型开始。与字符串字面量类型类似,如果我们将这些值其中之一分配给一个可变变量,这些类型就会消失,并通过称为拓宽(widening)的一种过程变成 string。
const n: number = 123;
// Has the type `${number}px`
const s1 = `${n}px`;
// Works!
const s2: `${number}px` = s1;
// Error!
const s3: `${number}pt` = s1;
// Has the type 'string' because of widening.
let v1 = s1;

详情查看拉取请求:

https://github.com/microsoft/TypeScript/pull/41891

更严格地检查 in 运算符
在 JavaScript 中,在 in 运算符的右侧使用一个非对象类型会出运行时错误。TypeScript 4.2 现在可以在设计时捕获它。
"foo" in 42
// ~~
// error! The right-hand side of an 'in' expression must not be a primitive.

这个检查在大多数情况下是相当保守的,因此如果你收到与此相关的错误,表明问题可能出在代码中。非常感谢我们的外部贡献者 Jonas Hübotter 的拉取请求:

https://github.com/microsoft/TypeScript/pull/41928

--noPropertyAccessFromIndexSignature
当 TypeScript 首次引入索引签名时,你只能使用“中括号”的元素访问语法(如 person["name"])来获得它们声明的属性。
interface SomeType {
    /** This is an index signature. */
    [propName: string]: any;
}
function doStuff(value: SomeType) {
    let x = value["someProperty"];
}
在需要使用具有任意属性的对象时,这个限制就很烦人了。例如,假设有一个 API 在末尾添加一个额外的 s 字符,结果搞错了属性名称。
interface Options {
    /** File patterns to be excluded. */
    exclude?: string[];
    /**
     * It handles any extra properties that we haven't declared as type 'any'.
     */

    [x: string]: any;
}
function processOptions(opts: Options) {
    // Notice we're *intentionally* accessing `excludes`, not `exclude`
    if (opts.excludes) {
        console.error("The option `excludes` is not valid. Did you mean `exclude`?");
    }
}

为了简化这类场景的操作,前不久 TypeScript 在类型带有一个字符串索引签名时加入了“点”属性访问语法(例如 person.name)。这样将原有 JavaScript 代码过渡到 TypeScript 也简单了一些。

但是,放宽这个限制也意味着人们会更容易拼错明确声明的属性。
function processOptions(opts: Options) {
    // ...
    // Notice we're *accidentally* accessing `excludes` this time.
    // Oops! Totally valid.
    for (const excludePattern of opts.excludes) {
        // ...
    }
}

在某些情况下,用户希望显式选择加入索引签名——当点属性访问与特定的属性声明不对应时,他们希望收到错误消息。

所以 TypeScript 引入了一个名为 --noPropertyAccessFromIndexSignature 的新标志。在这种模式下,你将选择使用 TypeScript 的旧款行为,跳出一个错误。这个新设置不受 strict 标志族的限制,因为我们相信用户会发现它在某些代码库上更好用。

详细信息请查看拉取请求,感谢 Wenlu Wang 的贡献。

https://github.com/microsoft/TypeScript/pull/40171/

abstract 构造签名
TypeScript 允许我们将一个类标记为 abstract。这会告诉 TypeScript 这个类只能被 extend,并且需要由任意子类填充特定成员才能实际创建实例。
abstract class Shape {
    abstract getArea(): number;
}
// Error! Can't instantiate an abstract class.
new Shape();
class Square extends Shape {
    #sideLength: number;
    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }
    getArea() {
        return this.#sideLength ** 2;
    }
}
// Works fine.
new Square(42);
为了确保在 abstract 类的新建过程中始终应用这一限制,你不能将 abstract 类分配给任何需要构造签名的对象。
interface HasArea {
    getArea(): number;
}
// Error! Cannot assign an abstract constructor type to a non-abstract constructor type.
let Ctor: new () => HasArea = Shape;
如果我们打算运行 new Ctor 这样的代码,这就是正确的做法;但如果我们想编写 Ctor 的子类,那就太过严格了。
functon makeSubclassWithArea(Ctor: new () => HasArea) {
    return class extends Ctor {
        getArea() {
            // ...
        }
    }
}
let MyShape = makeSubclassWithArea(Shape);
它也无法与 InstanceType 之类的内置辅助类型配合使用。
// Error!
// Type 'typeof Shape' does not satisfy the constraint 'new (...args: any) => any'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.
type MyInstance = InstanceType<typeof Shape>;
因此 TypeScript 4.2 允许你在构造函数签名上指定一个 abstract 修饰符。
interface HasArea {
    getArea(): number;
}
// Works!
let Ctor: abstract new () => HasArea = Shape;
// ^^^^^^^^

将 abstract 修饰符添加到一个构造签名,表示你可以传递 abstract 构造函数。这并不会阻止你传递其他“具体”的类 / 构造函数——它实际上只是表明没有意图直接运行构造函数,因此可以安全地传递任何一种类类型。

这个特性允许我们以支持抽象类的方式编写 mixin 工厂。例如,在以下代码片段中,我们可以将 mixin 函数 withStyles 与 abstract 类 SuperClass 一起使用。
abstract class SuperClass {
    abstract someMethod(): void;
    badda() {}
}
type AbstractConstructor = abstract new (...args: any[]) => T
function withStyles>(Ctor: T) {
    abstract class StyledClass extends Ctor {
        getStyles() {
            // ...
        }
    }
    return StyledClass;
}
class SubClass extends withStyles(SuperClass) {
    someMethod() {
        this.someMethod()
    }
}

请注意,withStyles 展示了一条特定的规则,那就是如果一个类(如 StyledClass)扩展一个泛型且受抽象构造函数(如 Ctor)限制的值,这个类也要声明为 abstract。这是因为我们无法知道是否传入了具有更多抽象成员的类,因此无法知道子类是否实现了所有抽象成员。

详情查看拉取请求:

https://github.com/microsoft/TypeScript/pull/36392

通过 --explainFiles 了解为什么文件已包含在程序中

对于 TypeScript 用户来说,一个常见的场景是询问“为什么 TypeScript 包含了这个文件?”。推断程序文件是一个复杂的过程,因此很多情况下程序会使用 lib.d.ts 的某种组合、包含 node_modules 中的某些文件,或者在你以为 exclude 掉某些文件时依旧将它们包含在内。

所以 TypeScript 现在提供了 --explainFiles 标志。
tsc --explainFiles
使用这个选项时,TypeScript 编译器将给出一些非常冗长的输出,说明文件为何会进入程序。想要读起来轻松一些的话,你可以将输出转发到一个文件,或将其传输给可以轻松查看它的程序里。
# Forward output to a text file
tsc --explainFiles > expanation.txt
# Pipe output to a utility program like `less`, or an editor like VS Code
tsc --explainFiles | less
tsc --explainFiles |
 code -
一般来说,输出将首先列出包含 lib.d.ts 文件的原因,然后是本地文件和 node_modules 文件。
TS_Compiler_Directory/4.2.0-beta/lib/lib.es5.d.ts
  Library referenced via 'es5' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2015.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2015.d.ts
  Library referenced via 'es2015' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2016.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2016.d.ts
  Library referenced via 'es2016' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2017.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2017.d.ts
  Library referenced via 'es2017' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2018.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2018.d.ts
  Library referenced via 'es2018' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2019.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2019.d.ts
  Library referenced via 'es2019' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2020.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2020.d.ts
  Library referenced via 'es2020' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.esnext.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.esnext.d.ts
  Library 'lib.esnext.d.ts' specified in compilerOptions
... More Library References...
foo.ts
  Matched by include pattern '**/*' in 'tsconfig.json'

目前我们无法保证输出格式不变——它可能会随着时间而改变。如果你有任何建议,可以告诉我们帮助改进这个格式!

有关更多信息,请查看原始的拉取请求:

https://github.com/microsoft/TypeScript/pull/40011

可选属性和字符串索引签名之间的规则放宽
字符串索引签名是一种类型化字典型对象的方式,你希望允许使用任意键访问:
const movieWatchCount: { [key: string]: number } = {};
function watchMovie(title: string) {
  movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1;
}
当然,对于尚未在字典中的任何电影标题,movieWatchCount[title] 将是 undefined 的(TypeScript 4.1 添加了选项 --noUncheckedIndexedAccess,从这样的索引签名中读取时包含 undefined)。很明显,movieWatchCount 中肯定不存在某些字符串,但由于存在 undefined,以前版本的 TypeScript 仍将可选对象属性视为无法分配给其他兼容的索引签名。
type WesAndersonWatchCount = {
  "Fantastic Mr. Fox"?: number;
  "The Royal Tenenbaums"?: number;
  "Moonrise Kingdom"?: number;
  "The Grand Budapest Hotel"?: number;
};
declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
// ~~~~~~~~~~~~~~~ error!
// Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.
// Property '"Fantastic Mr. Fox"' is incompatible with index signature.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'. (2322)
TypeScript 4.2 允许了这种分配。但是,它不允许分配类型有 undefined 的非可选属性,也不允许将 undefined 写入特定键:
type BatmanWatchCount = {
  "Batman Begins": number | undefined;
  "The Dark Knight": number | undefined;
  "The Dark Knight Rises": number | undefined;
};
declare const batmanWatchCount: BatmanWatchCount;
// Still an error in TypeScript 4.2.
// `undefined` is only ignored when properties are marked optional.
const movieWatchCount: { [key: string]: number } = batmanWatchCount;
// Still an error in TypeScript 4.2.
// Index signatures don't implicitly allow explicit `undefined`.
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;
新规则也不适用于数字索引签名,因为它们被假定为类似数组且密集的:
declare let sortOfArrayish: { [key: number]: string };
declare let numberKeys: { 42?: string };
// Error! Type '{ 42?: string | undefined; }' is not assignable to type '{ [key: number]: string; }'.
sortOfArrayish = numberKeys;

进一步了解请阅读原始拉取请求:

https://github.com/microsoft/TypeScript/pull/41921

声明缺少的助手函数

感谢 Alexander Tarasyuk 提出的社区拉取请求,我们现在有了一个快速修复程序,用于基于调用站点声明新函数和方法!

https://github.com/microsoft/TypeScript/pull/41215

重大更改

我们一直在努力减少新版中的重大更改数量。TypeScript 4.2 包含一些重大更改,但我们认为它们应该不会太影响升级过程。

 模板字面量表达式具有模板字面量类型
如前所述,模板字符串表达式现在以模板字面量类型开始。
const n: number = 123;
const s1 = `${n}px`; // `${number}px`
const s2: `${number}px` = s1;
const s3: `${number}pt` = s1; // Error
let v1 = s1; // string (because of widening)

这是一个突破,因为这些值过去仅有 string 类型。

更多信息参见相应的拉取请求:

https://github.com/microsoft/TypeScript/pull/41891

 noImplicitAny 错误,用于宽松的 yield 表达式
当捕获了一个 yield 表达式但没有在上下文中类型化它(也就是说 TypeScript 不知道类型是什么)时,TypeScript 现在将发出一个隐式的 any 错误。
function* g1() {
  const value = yield 1; // report implicit any error
}
function* g2() {
  yield 1; // result is unused, no error
}
function* g3() {
  const value: string = yield 1; // result is contextually typed by type annotation of `value`, no error.
}
function* g3(): Generator<number, void, string> {
  const value = yield 1; // result is contextually typed by return-type annotation of `g3`, no error.
}

在相应的更改中查看更多信息:

https://github.com/microsoft/TypeScript/pull/41348

 JavaScript 中的类型参数未解析为类型参数
JavaScript 中已经不允许使用类型参数,但在 TypeScript 4.2 中,解析器将以更符合规范的方式解析它们。因此,在 JavaScript 文件中编写以下代码时:
f(100)
TypeScript 将其解析为以下 JavaScript:
(f < T) > (100)

如果你利用 TypeScript 的 API 来解析 JavaScript 文件中的类型构造,这可能会对你造成影响。尝试解析 Flow 文件时就可能出现这种情况。

 in 运算符不再允许在右侧使用基元类型
如前所述,在 in 运算符的右侧使用基元是错误的,而 TypeScript 4.2 对于此类代码更加严格。
"foo" in 42
// ~~
// error! The right-hand side of an 'in' expression must not be a primitive.

更多信息参见拉取请求:

https://github.com/microsoft/TypeScript/pull/41928

 TypeScript 在 visitNode 中的 lift 回调使用其他类型

TypeScript 有一个接收 lift 函数的 visitNode 函数。现在,lift 需要一个 readonly Node[] 而不是 NodeArray。从技术上讲这是一个 API 重大更改,了解更多内容请移步下方地址:

https://github.com/microsoft/TypeScript/pull/42000

下一步计划

我们热切希望听到你对 TypeScript 4.2 的看法!我们仍处于早期测试阶段,但我们希望你能提供宝贵的反馈意见,帮助打造更出色的版本。请立刻试用吧,如果遇到任何问题请告诉我们!

 延伸阅读

https://devblogs.microsoft.com/typescript/announcing-typescript-4-2-beta/

浏览 42
点赞
评论
收藏
分享

手机扫一扫分享

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

手机扫一扫分享

分享
举报