TypeScript 4.3 beta 版本正式发布:新增import语句补全,对模板字符串类型进行改进...

作者 | TypeScript 团队

译者 | 王强

策划 | 田晓旭

来源|前端之巅

今天,我们很高兴为大家带来了 TypeScript 4.3 的 Beta 版本!

要开始使用这个 Beta 版本,可以通过 NuGet 获取:

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

也可以输入以下 npm 命令:

npm install typescript@beta

你还可以通过以下方式获得编辑器支持:

  • 下载 VisualStudio2019/2017

  • 按照针对 VisualStudioCode 和 SublimeText 的指示操作。

下面我们就来深入了解 TypeScript 4.3 带来的新内容吧!

属性上的单独写入类型

在 JavaScript 中,API 在存储之前转换传入的值是很常见的。这在 getter 和 setter 中也时常遇到。例如,假设我们有一个带有 setter 的类,其总是将值转换为一个 number,然后再保存在一个私有字段中。

class Thing {
#size = 0;

get size() {
return this.#size;
}
set size(value) {
let num = Number(value);
// Don't allow NaN and stuff.
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}

        this.#size = num;
}
}

在 TypeScript 中是怎样类型化这段 JavaScript 代码的呢?其实从技术上讲,我们在这里用不着专门做什么事情——TypeScript 用不着显式类型就能理解这段内容,并且可以理解 size 是一个 number。

问题是 size 允许你分配的不仅仅是 number。为了解决这个问题,我们可以说 size 具有 unknown 或 any 类型,就像下面的代码段这样:

class Thing {
// ...
get size(): unknown {
return this.#size;
}
}

但这种办法并不理想——unknown 会让人们在理解 size 的时候做一个类型断言,而 any 不会捕获任何错误。如果我们真的想对转换值的 API 建模,之前版本的 TypeScript 会要求我们在精确度(值读起来更轻松,写起来更难)和宽容度(值写起来更轻松,读起来更难)之间做出权衡。

因此,TypeScript 4.3 允许你分别指定用于读取(reading)和编写(writing)的属性类型。

class Thing {
#size = 0;

    get size(): number {
return this.#size;
}

    set size(value: string | number | boolean) {
let num = Number(value);

        // Don't allow NaN and stuff.
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}

        this.#size = num;
}
}

在上面的示例中,我们的 set 访问器采用了更广泛的类型集(string、boolean 和 number),但我们的 get 访问器始终保证它是一个 number。现在我们终于可以将其他类型分配给这些属性而不会产生错误了!

let thing = new Thing();
// Assigning other types to `thing.size` works!
thing.size = "hello";
thing.size = true;
thing.size = 42;
// Reading `thing.size` always produces a number!
let mySize: number = thing.size;

考虑两个具有相同名称的属性之间的关系时,TypeScript 将仅使用“读”类型(例如上面的 get 访问器上的类型),仅在直接写入属性时才考虑“写”类型。

请记住,这种模式并不只局限在类上。你可以在对象常量中编写具有不同类型的 getter 和 setter。

function makeThing(): Thing {
let size = 0;
return {
get size(): number {
return size;
},
set size(value: string | number | boolean) {
let num = Number(value);

            // Don't allow NaN and stuff.
if (!Number.isFinite(num)) {
size = 0;
return;
}

            size = num;
}
}
}

实际上,我们已经在接口 / 对象类型中添加了语法,以支持对属性的不同读 / 写类型。

// Now valid!
interface Thing {
get size(): number
set size(value: number | string | boolean);
}

使用不同类型来读和写属性时存在一个限制,那就是读属性的类型必须可分配给你正在编写的类型,换句话说,getter 类型必须可以分配给 setter。这就保证了一定程度的一致性,于是属性总是能分配给自身了。

要了解这个特性的更多信息,请查看实现的拉取请求:

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

override 和 --noImplicitOverride 标志

在 JavaScript 中扩展类时,因为语言设计的关系我们可以非常容易地覆盖方法。但不幸的是,你可能会遇到一些错误。

一大错误是缺少重命名,拿以下类为例:

class SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}

SpecializedComponent 扩展了 SomeComponent 的子类,并覆盖了 show 和 hide 方法,如果某人决定剥离 show 和 hide 并用一个单独的方法替换它们,会发生什么?

class SomeComponent {
- show() {
- // ...
- }
- hide() {
- // ...
- }
+ setVisible(value: boolean) {
+ // ...
+ }
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}

麻烦了,我们的 SpecializedComponent 没有更新。现在代码只是添加了这两个毫无用途的 show 和 hide 方法,它们可能根本不会被调用。

这里的问题有一部分是用户没有明确他们是要添加一个新方法还是要覆盖一个现有方法,这就是 TypeScript 4.3 添加 override 关键字的原因所在。

class SpecializedComponent extends SomeComponent {
override show() {
// ...
}
override hide() {
// ...
}
}

当一个方法被标记为 override 时,TypeScript 将始终确保基类中存在一个具有相同名称的方法。

class SomeComponent {
setVisible(value: boolean) {
// ...
}
}
class SpecializedComponent extends SomeComponent {
override show() {
// ~~~~~~~~
// Error! This method can't be marked with 'override' because it's not declared in 'SomeComponent'.
// ...
}

    // ...
}

这是一个很大的改进,但是如果你忘记在一个方法上编写 override,那么它也不会起作用——这也是用户可能会遇到的一大错误。

例如,你可能不小心“践踏”了基类中存在的一个方法,却毫不自知。

class Base {
someHelperMethod() {
// ...
}
}
class Derived extends Base {
// Oops! We weren't trying to override here,
// we just needed to write a local helper method.
someHelperMethod() {
// ...
}
}

这就是为什么 TypeScript 4.3 还提供了一个新的 --noImplicitOverride 标志。启用此选项时,除非你显式使用一个 override 关键字,否则重写一个超类中的任何方法将生成错误。在最后一个示例中,TypeScript 在 -noImplicitOverride 下将出错,并为我们提供一个线索,表明我们可能需要在 Derived 内部重命名方法。

我们要感谢我们的社区为这个实现做出的贡献。这些项目的实现是在 Wenlu Wang 的一个拉取请求中完成的:

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

而 Paul Cody Johnston 的一个更早的拉取请求只实现了 override 关键字,但奠定了思考和讨论的方向基础。我们向大家为这些特性做出的贡献表示敬意。

模板字符串类型的改进

在最近的版本中,TypeScript 引入了一种新的类型构造:模板字符串类型。这些类型可以通过级联来构造新的类似字符串的类型……

type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// same as
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";

……或匹配其他类似字符串类型的模式。

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
// Works!
s1 = s2;

我们所做的第一个更改是当 TypeScript 推断一个模板字符串类型时,当一个模板字符串由一个类似字符串字面量的类型在上下文中类型化时(例如,当 TypeScript 看到我们正在将一个模板字符串传递给接收一个字面量类型的对象),它将尝试为该表达式指定一个模板类型。

function bar(s: string): `hello ${string}` {
// Previously an error, now works!
return `hello ${s}`;
}

推断类型时也会这样做,并且类型参数 extends string

declare let s: string;
declare function f<T extends string>(x: T): T;
// Previously: string
// Now : `hello-${string}`
let x2 = f(`hello ${s}`);

这里的第二个主要更改是 TypeScript 现在可以更好地关联并推断不同的模板字符串类型。以下示例代码中可以看出:

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
s1 = s2;
s1 = s3;

当检查 s2 上的一个类似字符串字面量的类型时,TypeScript 可以匹配字符串内容,并确定 s2 在第一次分配中与 s1 兼容;但是,一旦看到另一个模板字符串,它就放弃了。于是像 s3 到 s1 这样的分配都不会生效。

现在,TypeScript 确实可以证明模板字符串的每个部分是否可以成功匹配。你现在可以混合使用不同的替换字符串来匹配模板字符串,TypeScript 可以很好地搞清楚它们是否真的兼容。

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
declare let s4: `1-${number}-3`;
declare let s5: `1-2-${number}`;
declare let s6: `${number}-2-${number}`;
// Now *all of these* work!
s1 = s2;
s1 = s3;
s1 = s4;
s1 = s5;
s1 = s6;

在进行这项工作时,我们自然也添加了更好的推理能力,拿下面这个示例来说:

declare function foo<V extends string>(arg: `*${V}*`): V;
function test<T extends string>(s: string, n: number, b: boolean, t: T) {
let x1 = foo("*hello*"); // "hello"
let x2 = foo("**hello**"); // "*hello*"
let x3 = foo(`*${s}*` as const); // string
let x4 = foo(`*${n}*` as const); // `${number}`
let x5 = foo(`*${b}*` as const); // "true" | "false"
let x6 = foo(`*${t}*` as const); // `${T}`
let x7 = foo(`**${s}**` as const); // `*${string}*`
}

要了解更多信息,请参阅原始的关于利用上下文类型的拉取请求:

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

以及改进推理和模板类型之间检查的拉取请求:

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

ECMAScript #private 类元素

TypeScript 4.3 可以为类中的更多元素赋予 #private #names,这样它们在运行时就能真正实现私有了。除了属性外,方法和访问器也可以赋予私有名称。

class Foo {
#someMethod() {
//...
}
get #someValue() {
return 100;
}
publicMethod() {
// These work.
// We can access private-named members inside this class.
this.#someMethod();
return this.#someValue;
}
}
new Foo().#someMethod();
// ~~~~~~~~~~~
// error!
// Property '#someMethod' is not accessible
// outside class 'Foo' because it has a private identifier.
new Foo().#someValue;
// ~~~~~~~~~~
// error!
// Property '#someValue' is not accessible
// outside class 'Foo' because it has a private identifier.

不仅如此,静态成员现在也可以具有私有名称。

class Foo {
static #someMethod() {
// ...
}
}
Foo.#someMethod();
// ~~~~~~~~~~~
// error!
// Property '#someMethod' is not accessible
// outside class 'Foo' because it has a private identifier.

此特性是在我们彭博社朋友的一个拉取请求中完成的,该请求由 Titian Cernicova-DragomirandKubilay Kahveci 发起,并得到了 Joey Watts、Rob Palmer 和 Tim McClure 的专业支持。我们对各位表示诚挚的谢意!

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

永远 truthy 的 promise 检查

在 strictNullChecks 下,检查一个条件中的一个 Promise 是否“真实”会触发错误。

async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
if (foo()) {
// ~~~~~
// Error!
// This condition will always return true since
// this 'Promise<boolean>' appears to always be defined.
// Did you forget to use 'await'?
return "true";
}
return "false";
}

此更改由 JackWorks 贡献,我们对他们表示感谢!

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

static 索引签名

索引签名使我们可以在一个值上设置比一个类型显式声明更多的属性。

class Foo {
hello = "hello";
world = 1234;

    // This is an index signature:
[propName: string]: string | number | undefined;
}
let instance = new Foo();
// Valid assigment
instance["whatever"] = 42;
// Has type 'string | number | undefined'.
let x = instance["something"];

之前,索引签名只能在类的实例侧声明。感谢来自 Wenlu Wang 的拉取请求,现在我们可以将索引签名声明为 static。

class Foo {
static hello = "hello";
static world = 1234;
static [propName: string]: string | number | undefined;
}
// Valid.
Foo["whatever"] = 42;
// Has type 'string | number | undefined'
let x = Foo["something"];

适用于索引签名的规则现在在类的静态侧和实例侧都是一样的,也就是说,其他所有静态属性都必须与索引签名兼容。

class Foo {
static prop = true;
// ~~~~
// Error! Property 'prop' of type 'boolean'
// is not assignable to string index type
// 'string | number | undefined'.

    static [propName: string]: string | number | undefined;
}

import 语句补全

用户在 JavaScript 中使用 import 和 export 语句时,遇到的最大痛苦之一就是顺序——特别是导入被编写为:

import { func } from "./module.js";

而不是:

from "./module.js" import { func };

从头开始编写完整的 import 语句时,这就会给人带来一些麻烦,因为自动完成功能无法正常工作。例如,如果你开始编写类似 import{这样的内容,TypeScript 就没法知道你打算从哪个模块导入,因此它无法提供任何缩小范围的补全。

为了缓解这种问题,我们利用了 auto-import 的能力!Auto-import 已经解决了无法缩小特定模块的补全范围的问题——原理是提供所有可能的导出并自动在你的文件顶部插入一个导入语句。

因此,当你现在开始编写没有路径的 import 语句时,我们将为你提供可能的导入列表。当你提交一个补全后,我们会完成完整的导入语句,其中包括你要编写的路径。

这项工作需要编辑器专门支持该特性。你可以使用最新的 Insiders 版本的 Visual Studio Code 进行尝试。

要了解更多信息,请查看实现的拉取请求:

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

@link 标签的编辑器支持

TypeScript 现在可以理解 @link 标签,并尝试解析它们链接的声明。这意味着你可以将鼠标悬停在 @link 标记内的名称上并获取简要信息,或使用 go-to-definition 或 find-all-references 之类的命令。

例如,在下面的示例中,你能在 @link bar 中的 bar 上 go-to-definition,支持 TypeScript 的编辑器将跳转到 bar 的函数声明。

/**
* This function depends on {@link bar}
*/
function foo() {
}
function bar() {
}

要了解更多信息,请参见 GitHub 上的拉取请求:

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

重大更改

lib.d.ts 的更改

与每个 TypeScript 版本一样,lib.d.ts 的声明(特别是为 Web 上下文生成的声明)已更改。在此版本中,我们利用 Mozilla 的 browser-compat-data 移除了没有浏览器实现的 API。lib.d.ts 已经移除了 Account、AssertionOptions、RTCStatsEventInit、MSGestureEvent、DeviceLightEvent、MSPointerEvent、ServiceWorkerMessageEvent 和 WebAuthentication 之类你很难用到的 API。详细信息在这里讨论:

https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/991

Always-Truthy 的 promise 检查上的错误

在 strictNullChecks 下,使用始终在一个条件检查中被定义的 Promise 现在被视为错误。

declare var p: Promise<number>;
if (p) {
// ~
// Error!
// This condition will always return true since
// this 'Promise<number>' appears to always be defined.
//
// Did you forget to use 'await'?
}

要了解更多细节,请参见原始更改:

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

Union Enum 不能与任意数字对比

当某些 enum 的成员被自动填充或平凡编写时,它们会被视为联合 enum,在这种情况下,一个 enum 可以重新调用它可能表示的每个值。

在 TypeScript 4.3 中,如果将具有一个联合 enum 类型的值与一个不可能相等的数字字面量进行比较,则类型检查器将发出错误。

enum E {
A = 0,
B = 1,
}
function doSomething(x: E) {
// Error! This condition will always return 'true' since the types 'E' and '-1' have no overlap.
if (x === -1) {
// ...
}
}

解决方法是,你可以重新编写一个注解以包括适当的字面量类型。

enum E {
A = 0,
B = 1,
}
// Include -1 in the type, if we're really certain that -1 can come through.
function doSomething(x: E | -1) {
if (x === -1) {
// ...
}
}

你还可以在值上使用一个类型断言。

enum E {
A = 0,
B = 1,
}
function doSomething(x: E) {
// Use a type asertion on 'x' because we know we're not actually just dealing with values from 'E'.
if ((x as number) === -1) {
// ...
}
}

另外,你可以重新声明你的 enum,使其具有一个非平凡的初始化器,以便任何数字都可以被分配并与该 enum 进行比较。如果你的意图是让 enum 指定一些大家都知道的值,那么这个更改可能会很有用。

enum E {
// the leading + on 0 opts TypeScript out of inferring a union enum.
A = +0,
B = 1,
}

要了解更多细节,请参见原始更改:

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

下一步计划

你可以检查 TypeScript 4.3 迭代计划来跟踪即将发布的候选版本和稳定版本。我们希望获得关于该 Beta 版本的反馈(反馈我们的 nightly 版本就更好了),因此请立即试用!

https://github.com/microsoft/TypeScript/issues/42762

编程快乐!

——Daniel Rosenwasser 和 TypeScript 团队

 延伸阅读

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

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

》》面试官也在看的算法资料《《

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值