TypeScript 3.9正式发布!平均编译时长从26秒缩短至10秒

2020年5月13日 284点热度 0人点赞 0条评论
图片
作者 | 微软官方博客
译者 | 核子可乐
策划 | 小智
今天,微软在其官方博客宣布:TypeScript 3.9 版本已经正式发布,详情见下文。

有些朋友可能对 TypeScript 还不太熟悉,这是一种以 JavaScript 为基础开发的语言,新增 type 声明与注释等多种语法。TypeScript 编译器能够使用这些语法对代码进行 type 检查,而后输出能够适配多种不同运行时、且清晰可读的 JavaScript 代码。

由于 TypeScript 具有丰富的跨编辑器功能,因此其中的静态 type 检查能够在代码运行甚至文件保存之前快速指示代码中存在的错误。除了错误检查之外,TypeScript 还允许用户在自己熟悉的编辑器中为 TypeScript 以及 JavaScript 代码提供补全、快速修复以及重构等功能。事实上,如果你曾经使用过 Visual Studio 或者 Visual Studio Code,那么以往的 JavaScript 代码编写体验中可能就已经有 TypeScript 的贡献了。如果希望了解更多详细信息,请 访问我们的网站。

而如果你已经在项目当中使用过 TypeScript,那么直接通过以下 npm 命令或者通过 NuGet 都能快速获取我们发布的本次新版本:
npm install typescript

当然,你还可以通过以下方式获取编辑器支持:

下载 Visual Studio 2019/2017 对应版本;

https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.TypeScript-39

安装 Visual Studio Code Insiders 版本;

http://code.visualstudio.com/insiders

或者通过链接使用 TypeScript 新版本;

https://code.visualstudio.com/docs/typescript/typescript-compiling\l_using-newer-typescript-versions

配合 Sublime Text 3 使用 PackageControl。

https://packagecontrol.io/packages/TypeScript

在此次新版本中,我们的团队高度关注性能表现、细节处理与稳定性。我们一直努力提高编译器速度与编辑体验,摆脱卡顿与繁琐的细节,同时减少 bug 与系统崩溃问题。当然,我们也从外部社区收到了很多有价值的功能与修复贡献。

Inference 与 Promise.all 迎来改进
TypeScript 的最近几个版本(3.7 及之后)已经对 Promise.all 及 Promise.race 等函数的声明做出更新。遗憾的是,更新带来了新的问题,这一点在混合 null 或 undefined 值时体现得尤其明显。
interface Lion {

    roar(): void

}

interface Seal {

    singKissFromARose(): void

}

async function visitZoo(lionExhibit: Promise<Lion>, sealExhibit: Promise<Seal | undefined>) {

    let [lion, seal] = await Promise.all([lionExhibit, sealExhibit]);

    lion.roar(); // uh oh

// ~~~~

// Object is possibly 'undefined'.

}

这种情况非常奇怪!事实上,sealExhibit 当中包含的 undefined,相当于是把 undefined 错误引入了 lion type 当中。

感谢 Jack Bates 提交的贡献,现在这个问题已经在 TypeScript 3.9 版本中得到修复。以上错误不复存在,如果大家仍在较早版本的 TypeScript 面临 Promise 的困扰,我们建议您尽快升级至 3.9 版本!

关于 awaited  type 的变化

如果大家一直在关注我们的问题跟踪器与设计研讨记录,可能已经注意到我们正在开发一种名为 awaited 的全新 type 操作符。该操作符的作用是准确对 JavaScript 中的 Promise 展开方式进行建模。

我们最初预计在 TypeScript 3.9 版本中发布 awaited,但在使用现有代码库运行早期 TypeScript build 时,我们意识到这项功能还需要进一步打磨才能正式交付。因此,我们决定将该功能从主分支中剥离出来,直到其做好服务用户的一切准备。我们将对这项功能进行更多试验,因此在 3.9 版本中 awaited 将暂时制度。

速度改进

TypeScript 3.9 将带来一系列新的速度改进机制。在发现 Material-ui 与 Styled-Components 等组件会带来极差的编辑 / 编译速度后,我们的团队一直努力进行性能优化。我们在这方面进行了深入研究,并提交多项 pull 请求以优化涉及大型联合、交集点、条件 type 以及映射 type 的性能问题。

  • https://github.com/microsoft/TypeScript/pull/36576

  • https://github.com/microsoft/TypeScript/pull/36590

  • https://github.com/microsoft/TypeScript/pull/36607

  • https://github.com/microsoft/TypeScript/pull/36622

  • https://github.com/microsoft/TypeScript/pull/36754

  • https://github.com/microsoft/TypeScript/pull/36696

在部分代码库上,相关 pull 请求的编译时间平均减少了 5% 至 10%。总体而言,我们已经将 material-ui-styles 项目的编译时间缩短约 25%。此外,我们还收到来自微软团队的反馈意见,他们表示 TypeScript 3.9 的平均编译时长由 26 秒缩短至 10 秒左右。

我们还对编辑器方案中的文件重命名功能做出几项调整。根据 Visual Studio Code 团队提供的建议,我们发现在执行文件重命名时,单是查明哪些导入语句需要更新就要耗去 5 到 10 秒时间。TypeScript 3.9 调整了内部编译器与语言服务缓存文件的查找方式,顺利解决了这个问题。

虽然仍有改善空间,但我们希望目前的成果能够为广大用户带来更好的使用体验!

// @ts-expect-error 注释
想象一下,如果我们正使用 TypeScript 编写一个库,并将名为 doSTuff 的函数作为公共 API 的一部分进行导出。该函数的 type 声明需要两个 strings,以便其他 TypeScript 用户正常获取 type-checking 错误。但与此同时,它还需要执行运行时检查(可能仅在开发 build 中)以向 JavaScript 用户提示错误信息。
function doStuff(abc: string, xyz: string) {

    assert(typeof abc === "string");

    assert(typeof xyz === "string");

    // do some stuff

}
因此一旦发生操作失误,TypeScript 用户面对的将是一条标红的乱码信息外加一条错误信息。而 JavaScript 用户则面对一条断言错误。我们希望通过单元测试检查实际情况与预期是否相符。
expect(() => {

    doStuff(123, 456);

}).toThrow();
遗憾的是,我们的测试是由 TypeScript 编写而成,而 TypeScript 只能提示一条错误信息!
doStuff(123, 456);

//          ~~~

// error: Type 'number' is not assignable to type 'string'.

为此,TypeScript 3.9 带来了新功能:// @ts-expect-error 注释。在一行代码以 // @ts-expect-error 注释作为前缀时,TypeScript 会禁止报告该错误。而如果没有发生错误,TypeScript 则报告不需要 // @ts-expect-error。

在以下简单示例代码中,一切正常运行:
// @ts-expect-error

console.log(47 * "octopus");

但下列代码:

// @ts-expect-error

console.log(1 + 1);
会导致错误:
Unused '@ts-expect-error' directive.

我们要特别感谢此项功能的贡献者 Josh Goldberg。关于更多详细信息,请参阅 ts-expect-error pull 请求:

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

ts-ignore 还是 ts-expect-error?

在某种程度上讲,// @ts-expect-error 可以作为抑制注释使用,其效果类似于 // @ts-ignore。但二者的区别在于,如果下一行代码没有错误,则 // @ts-ignore 不会发挥任何作用。

大家可能打算把现有 // @ts-ignore 注释变更为 // @ts-expect-error,而且好奇哪种方法更适合用于后续代码编写。虽然具体选择取决于您和您的团队,但这里我们还是整理出了一些相对普适的选择思路。

如果符合以下条件,请选择 ts-expect-error :

  • 您正在编写测试代码,且希望 type 系统在单一操作上显示错误。

  • 您希望尽快获得修复方法,只要能解决问题就行。

  • 您的项目规模合理,团队工作态度积极主动,希望在受影响代码恢复正常之后马上删除抑制注释。

如果符合以下条件,请选择 ts-ignore :

  • 您的项目规模很大大,而且在缺少明确归属的代码中出现了新错误。

  • 您正在两种不同 TypeScript 版本之间升级,某行代码只在其中一个版本上出现了错误。

  • 您根本没有时间认真考虑这两个选项中哪个更好。

在条件表达式中检查未调用函数

在 TypeScript 3.7 版本中,我们引入了未调用函数检查(uncalled function checks)以提示那些您忘记调用的函数。

function hasImportantPermissions(): boolean {

    // ...

}

// Oops!

if (hasImportantPermissions) {

// ~~~~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead?

    deleteAllTheImportantFiles();

}
然而,这种错误只适用于 if 语句。感谢 Alexander Tarasyuk 的贡献,现在此项功能已经能够正常支持三种条件(即 cond ? trueExpr : falseExpr 语法)。
declare function listFilesOfDirectory(dirPath: string): string[];
declare function isDirectory(): boolean;
function getAllFiles(startFileName: string) {

    const result: string[] = [];
    traverse(startFileName);
    return result;

    function traverse(currentPath: string) {

        return isDirectory ?

        // ~~~~~~~~~~~
        // This condition will always return true
        // since the function is always defined.
        // Did you mean to call it instead?

            listFilesOfDirectory(currentPath).forEach(traverse) :
            result.push(currentPath);

    }

}

Alexander 还进一步提交了快速修复方案,旨在改善未调用函数检查功能的使用体验!

图片

编辑器改进

TypeScript 编译器不只增强了大部分主流编辑器中的 TypeScript 编辑体验,同时也增强了 Visual Studio 系列编辑器中的 JavaScript 开发体验。根据您所使用的具体编辑器,新的 TypeScript/JavaScript 功能也会有所不同。以下为几项共通性改进:

  • Visual Studio Code 现在允许您选择不同的 TypeScript 版本。此外,JavaScript/TypeScript Nightly Extension 也将始终保持最新(通常相当稳定)。

  • Visual Studio 2017/2019 迎来最新版本的 SDK 安装器与 MSBuild 安装程序。

  • Sublime Text 3 支持用户选择不同 TypeScript 版本。

JavaScript 中的 CommonJS 自动补全

新版本的另一项重大改进,是使用 CommonJS 模块自动导入 JavaScript 文件。

在旧版本中,TypeScript 强制要求用户无论使用什么文件,都必须以 ECMAScript 的形式导入,例如:
import * as fs from "fs";
但在编写 JavaScript 文件时,很多用户并不打算使用 ECMScript 样式模块。不少朋友仍在使用 CommonJS 样式的 require(...) 导入,例如:
const fs = require("fs");

TypeScript 现在能够自动检测您所使用的导入类型,保证文件样式简洁而统一。

关于更多详细信息,请参阅相应 pull 请求:

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

代码操作保留换行符

TypeScript 的重构与快速修复往往无法正确保留换行符。先来看以下简单代码示例:

const maxValue = 100;

/*start*/

for (let i = 0; i <= maxValue; i++) {

    // First get the squared value.

    let square = i ** 2;

    // Now print the squared value.

    console.log(square);

}

/*end*/
如果我们在编辑器中从 /*start*/ 到 /*end*/ 的高亮显示区域内提取一条新函数,则最终得出的代码将如下所示:
const maxValue = 100;

printSquares();

function printSquares() {

    for (let i = 0; i <= maxValue; i++) {

        // First get the squared value.

        let square = i ** 2;

        // Now print the squared value.

        console.log(square);

    }

}

图片

这就不对了——原本 for 循环中的每个语句间都有一个空白行,但重构之后空白行消失了!好消息是,TypeScript 在保持编写内容准确性方面做出不少改进。
const maxValue = 100;

printSquares();

function printSquares() {

    for (let i = 0; i <= maxValue; i++) {

        // First get the squared value.

        let square = i ** 2;

        // Now print the squared value.

        console.log(square);

    }

}

图片

关于更多详细信息,请参阅相应 pull 请求:

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

快速修复缺失的返回表达式
在某些情况下,大家很可能会忘记返回函数中最后一条语句的值。这种情况在向箭头函数添加大括号时体现得尤其明显。
// before

let f1 = () => 42

// oops - not the same!

let f2 = () => { 42 }

感谢社区成员 Wenlu Wang 的贡献,TypeScript 现在获得了快速修复功能,可添加缺失的 return 语句、删除大括号或者为对象字面量等箭头函数实体添加括号。

图片

支持 “Solution Style” tsconfig.json 文件

编辑器需要确定当前文件属于哪个配置文件,以及当前“项目”中还包含哪些其他文件,从而选择适当的选项。在默认情况下,由 TypeScript 语言服务器支持的编辑器会在各个父目录中查找 tsconfig.json 以实现这一目的。

但问题在于,某些简单 tsconfig.json 会直接引用其他 tsconfig.json 文件。

// tsconfig.json

{

    "files": [],

    "references": [

        { "path": "./tsconfig.shared.json" },
        { "path": "./tsconfig.frontend.json" },
        { "path": "./tsconfig.backend.json" },

    ]

}

换句话说,这个文件的作用只是管理其他项目文件;在某些环境中,我们将这类文件称为“solution”。很明显,服务器无法正确提取这些 tsconfig.*.json 文件,但我们的目标正是让语言服务器意识到当前.ts 文件可能归属于 tsconfig.json 根目录所提及的其他项目。

TypeScript 3.9 解决了这个支持问题。关于更多详细信息,请参阅 相应的 pull 请求。

重大变化

解析可选链与非 null 断言中的差异

TypeScript 最近实现了对可选链操作符的支持,但根据用户反馈,非 null 断言操作符(!)的可选链(?.)行为不符合直觉。

具体来讲,在以往的版本中,代码:
foo?.bar!.baz
被解释为等效于以下 JavaScript 代码:
(foo?.bar).baz

在以上代码中,括号会阻止可选链的“短路”行为;因此如果未定义 foo 为 undefined,则访问 baz 会引发运行时错误。

发现这一问题的 Babel 团队以及向我们提交反馈的大部分其他用户,都认为这样的行为属于设计失误。我们完全认同大家的看法!根据群众和我们自己的内部意见,由于操作目的是从 bar type 中删除 null 与 undefined,因此!操作符应该直接“消失”。

换句话说,大多数人认为以上原始代码片段应该被解释为在:
foo?.bar.baz

中,当 foo 为 undefined 时,计算结果为 undefined。

这是一项重大变化,但我们认为大部分代码在编写时都是为了考虑新的解释场景。如果您希望继续使用旧有行为,则可在!操作符左侧添加括号,如下所示:
(foo?.bar)!.baz

} 与 > 现在为无效的 JSX 文本字符

JSX 规范禁止在文本位置中使用}与>字符,TypeScript 与 Babel 也遵循相同的规则。要在新版本中插入这些字符,您需要使用 HTML 转义代码 (例如 <div>2 &gt; 1</div> ) 或者插入一个带有字符串字面值的表达式 (例如 <div>2 {">"} 1</div> )。

幸运的是,由于 Brad Zacher 提交的 pull 请求,现在直接使用这两个符号会弹出以下错误提示:
Unexpected token. Did you mean `{'>'}` or `&gt;`?

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
例如:
let directions = <div>Navigate to: Menu Bar > Tools > Options</div>

// ~ ~

// Unexpected token. Did you mean `{'>'}` or `&gt;`?

这条错误消息中还附带便捷的快速修复功能,感谢 Alexander Tarasyuk 的贡献,您的努力让批量处理错误修复变得非常轻松。


更严格地检查交集与可选属性
一般来说,如果 A 和 B 中的任何一个可被赋值给 C,那么像 A 与 B 这样的交集 type 就可以被赋值给 C;但有时候,可选属性会引发问题。例如:
interface A {

    a: number; // notice this is 'number'

}

interface B {

    b: string;

}

interface C {

    a?: boolean; // notice this is 'boolean'
    b: string;

}

declare let x: A & B;
declare let y: C;

y = x;

在之前的 TypeScript 版本中,上述代码能够正常运行,因为 A 与 C 完全不兼容,而 B 与 C 兼容。

在 TypeScript 3.9 中,只要交集中的每个 type 都是一个具体的对象 type,则 type 系统将同时考虑所有属性。因此,TypeScript 会意识到 A&B 中的 A 属性与 C 不兼容:
Type 'A & B' is not assignable to type 'C'.

  Types of property 'a' are incompatible.

    Type 'number' is not assignable to type 'boolean | undefined'.

若需了解更多详细信息,请参阅相应 pull 请求:

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


通过属性判断减少交集
在某些情况下,我们的 type 可能会描述并不存在的值,例如:
declare function smushObjects<T, U>(x: T, y: U): T & U;

interface Circle {

    kind: "circle";
    radius: number;

}

interface Square {

    kind: "square";
    sideLength: number;

}

declare let x: Circle;
declare let y: Square;
let z = smushObjects(x, y);

console.log(z.kind);

这段代码有点奇怪,因为我们实际上没有办法为 Circle 与 Square 创建出交集——二者拥有两个互不兼容的 kind 字段。在之前的 TypeScript 版本中,这段代码可以正常运行,只是 kind 本身由于 "circle" & "square" 描述的值集不可能存在而被解释为 never 。

在 TypeScript 3.9 当中,type 系统变得更为严格——它会意识到 Circle 与 Square 因为 kind 属性的不同而不可能存在交集。因此不同于旧版本将 z.kind type 折叠为 never,新版本会将 z type 本身(Circle & Square)折叠为 never。这意味着以上代码现在将提示以下错误:
Property 'kind' does not exist on type 'never'.

通过观察,我们发现大多数中断都由 type 声明中的瑕疵引发。若需了解更多详细信息,请参阅原始 pull 请求:

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


Getters/Setters 不再属于可枚举属性

在 TypeScript 旧版本中,类中的 get 与 set 访问器会以可枚举形式发出;但这明显不符合 ECMAScript 规范。该规范要求将二者设定为不可枚举属性。因此,针对 ES5 与 ES2015 的 TypeScript 代码可能在实际执行中引发不同的行为。

感谢 GitHub 用户 pathurs 的贡献,TypeScript 3.9 已经在这方面向 ECMAScript 的要求看齐。


扩展 any 的 Type 参数不再作为 any 执行
在 TypeScript 的旧版本中,受 any 约束的 type 参数可被视为 any。
function foo<T extends any>(arg: T) {

    arg.spfjgerijghoied; // no error!

}
这是一项明显的疏忽,因此 TypeScript 3.9 采用了更保守的方法,将针对这些有问题的操作发出错误提示。
function foo<T extends any>(arg: T) {

    arg.spfjgerijghoied;

    // ~~~~~~~~~~~~~~~
    // Property 'spfjgerijghoied' does not exist on type 'T'.

}

始终保留 export *

在此前的 TypeScript 版本当中,如果 foo 没有导出任何值,则 export * from "foo"这类声明会在 JavaScript 输出结果中被直接删除。但这种处理方法并不完善,因为它是 type 定向的且无法被 Babel 模拟。TypeScript 3.9 将始终保留 export * 声明。在实践中,这项调整应该不会对代码造成太多实际影响,但捆绑程序对代码进行摇树时难度可能会有所提升。

若需了解更多版本变化,请参阅原始 pull 请求:

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


导出当前用户 Getters 以实现活动绑定

当我们在 ES5 及以上版本中以 CommonJS 等模块系统为目标时,TypeScript 会使用 get 访问器以模拟活动绑定,以便在任意导出模块中都可体现对单一模块内变量的更改。此次变更的目标,在于进一步改善 TypeScript 输出代码与 ECMAScript 模块的兼容度。

关于更多详细信息,请参阅 这项变更的 pull 请求:

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


导出结果的提升与初始赋值
配合 ES5 及更高版本中的 CommonJS 等目标模块系统,TypeScript 现在能够将导出的声明提升至文件顶部。这一改变意味着 TypeScript 的导出结果与 ECMAScript 模块将更加兼容。代码示例如下:
export * from "mod";

export const nameFromMod = 0;
此前的输出结果为:
__exportStar(exports, require("mod"));

exports.nameFromMod = 0;
但由于导出结果现在使用 get- 访问器,__exportStar 的存在使得赋值操作因该访问器无法被赋值简单覆盖而失败。在 TypeSCript 3.9 中,您需要使用以下命令:
exports.nameFromMod = void 0;

__exportStar(exports, require("mod"));

exports.nameFromMod = 0;

若需了解详细信息,请参阅原始 pull 请求:

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

下一阶段目标

我们希望 TypeScript 3.9 能进一步提升您的日常开发体验并加快开发速度。关于后续版本,欢迎大家关注我们的 4.0 迭代计划与功能发展路线图。

4.0迭代计划:https://github.com/microsoft/TypeScript/issues/38510

功能发展路线图:https://github.com/Microsoft/TypeScript/wiki/Roadmap


延伸阅读

https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/


活动推荐

GMTC 全球大前端技术大会(北京站)2020 关注前端、移动、AI 应用等多个技术领域,大会聚焦前沿技术及实践经验,旨在帮助参会者了解大前端 & 移动开发领域的技术趋势与实践案例。大会目前 8 折优惠报名,限时立减 960 元!了解更多大会内容,可扫描下图二维码或点击【阅读原文】。联系票务经理鱼丸:13269078023(同微信)

图片

45040TypeScript 3.9正式发布!平均编译时长从26秒缩短至10秒

这个人很懒,什么都没留下

文章评论