2023TypeScript 5.0 正式发布!置顶精帖
 goingdown  分类:教程资讯  人气:4701  回帖:0  发布于1年前 收藏

2023 年 3 月 17 日,TypeScript 5.0 正式发布!此版本带来了许多新功能,旨在使 TypeScript 更小、更简单、更快。TypeScript 5.0 实现了新的装饰器标准、更好地支持 Node 和打构建工具中的 ESM 项目的功能、库作者控制泛型推导的新方法、扩展了 JSDoc 功能、简化了配置,并进行了许多其他改进。

可以通过以下 npm 命令开始使用 TypeScript 5.0:

npm install -D typescript 复制代码


以下是 TypeScript 5.0 的主要更新:

  • 全新装饰器

  • const 类型参数

  • extends 支持多配置文件

  • 所有枚举都是联合枚举

  • --moduleResolutionbundler

  • 自定义解析标志

  • --verbatimModuleSyntax

  • 支持 export type *

  • JSDoc 支持 @satisfies

  • JSDoc 支持 @overload

  • 编辑器中不区分大小写的导入排序

  • 完善 switch/case

  • 优化速度、内存和包大小

  • 其他重大更改和弃用

全新装饰器

装饰器是即将推出的 ECMAScript 特性,它允许我们以可重用的方式自定义类及其成员。

考虑以下代码:

class Person {     name: string;     constructor(name: string) {         this.name = name;     }     greet() {         console.log(`Hello, my name is ${this.name}.`);     } } const p = new Person("Ray"); p.greet(); 复制代码


这里的 greet 方法很简单,在实际中它内部可能会跟复杂,比如需要执行异步逻辑,或者进行递归,亦或是有副作用等。那就可能需要使用 console.log 来调试 greet

class Person {     name: string;     constructor(name: string) {         this.name = name;     }     greet() {         console.log("LOG: Entering method.");         console.log(`Hello, my name is ${this.name}.`);         console.log("LOG: Exiting method.")     } } 复制代码


如果有一种方法可以为每种方法做到这一点,可能会很好。

这就是装饰器的用武之地。我们可以编写一个名为 loggedMethod 的函数,如下所示:

function loggedMethod(originalMethod: any, _context: any) {     function replacementMethod(this: any, ...args: any[]) {         console.log("LOG: Entering method.")         const result = originalMethod.call(this, ...args);         console.log("LOG: Exiting method.")         return result;     }     return replacementMethod; } 复制代码


这里用了很多 any,可以暂时忽略,这样可以让例子尽可能得简单。

这里,loggedMethod 需要传入一个参数(originalMethod) 并返回一个函数。执行过程如下:

  1. 打印:LOG: Entering method.

  2. 将 this 及其所有参数传递给原始方法

  3. 打印:LOG: Exiting method.

  4. 返回原始方法的执行结果

现在我们就可以使用 loggedMethod 来修饰 greet 方法:

class Person {     name: string;     constructor(name: string) {         this.name = name;     }     @loggedMethod     greet() {         console.log(`Hello, my name is ${this.name}.`);     } } const p = new Person("Ray"); p.greet(); 复制代码


输出如下:

LOG: Entering method. Hello, my name is Ray. LOG: Exiting method. 复制代码


这里我们在 greet 上面使用了 loggedMethod 作为装饰器——注意这里的写法:@loggedMethod。这样,它会被原始方法和 context 对象调用。因为 loggedMethod 返回了一个新函数,该函数替换了 greet 的原始定义。

loggedMethod 的第二个参数被称为“ context 对象”,它包含一些关于如何声明装饰方法的有用信息——比如它是 #private 成员还是静态成员,或者方法的名称是什么。 下面来重写 loggedMethod 以利用它并打印出被修饰的方法的名称。

function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {     const methodName = String(context.name);     function replacementMethod(this: any, ...args: any[]) {         console.log(`LOG: Entering method '${methodName}'.`)         const result = originalMethod.call(this, ...args);         console.log(`LOG: Exiting method '${methodName}'.`)         return result;     }     return replacementMethod; } 复制代码


TypeScript 提供了一个名为 ClassMethodDecoratorContext 的类型,它对方法装饰器采用的 context 对象进行建模。除了元数据之外,方法的 context 对象还有一个有用的函数:addInitializer。 这是一种挂接到构造函数开头的方法(如果使用静态方法,则挂接到类本身的初始化)。

举个例子,在JavaScript中,经常会写如下的模式:

class Person {     name: string;     constructor(name: string) {         this.name = name;         this.greet = this.greet.bind(this);     }     greet() {         console.log(`Hello, my name is ${this.name}.`);     } } 复制代码


或者,greet可以声明为初始化为箭头函数的属性。

class Person {     name: string;     constructor(name: string) {         this.name = name;     }     greet = () => {         console.log(`Hello, my name is ${this.name}.`);     }; } 复制代码


编写这段代码是为了确保在greet作为独立函数调用或作为回调函数传递时不会重新绑定。

const greet = new Person("Ray").greet; greet(); 复制代码


可以编写一个装饰器,使用addInitializer在构造函数中为我们调用 bind

function bound(originalMethod: any, context: ClassMethodDecoratorContext) {     const methodName = context.name;     if (context.private) {         throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);     }     context.addInitializer(function () {         this[methodName] = this[methodName].bind(this);     }); } 复制代码


bound不会返回任何内容,所以当它装饰一个方法时,它会保留原来的方法。相反,它会在其他字段初始化之前添加逻辑。

class Person {     name: string;     constructor(name: string) {         this.name = name;     }     @bound     @loggedMethod     greet() {         console.log(`Hello, my name is ${this.name}.`);     } } const p = new Person("Ray"); const greet = p.greet; greet(); 复制代码


注意,我们使用了两个装饰器:@bound@loggedMethod。这些装饰是以“相反的顺序”运行的。也就是说,@loggedMethod修饰了原始方法greet@bound修饰了@loggedMethod的结果。在这个例子中,这没有关系——但如果装饰器有副作用或期望某种顺序,则可能有关系。

可以将这些装饰器放在同一行:

@bound @loggedMethod greet() { console.log(`Hello, my name is ${this.name}.`); } 复制代码


我们甚至可以创建返回装饰器函数的函数。这使得我们可以对最终的装饰器进行一些自定义。如果我们愿意,我们可以让loggedMethod返回一个装饰器,并自定义它记录消息的方式。

function loggedMethod(headMessage = "LOG:") {     return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {         const methodName = String(context.name);         function replacementMethod(this: any, ...args: any[]) {             console.log(`${headMessage} Entering method '${methodName}'.`)             const result = originalMethod.call(this, ...args);             console.log(`${headMessage} Exiting method '${methodName}'.`)             return result;         }         return replacementMethod;     } } 复制代码


如果这样做,必须在使用loggedMethod作为装饰器之前调用它。然后,可以传入任何字符串作为记录到控制台的消息的前缀。

class Person {     name: string;     constructor(name: string) {         this.name = name;     }     @loggedMethod("")     greet() {         console.log(`Hello, my name is ${this.name}.`);     } } const p = new Person("Ray"); p.greet(); 复制代码


输出结果如下:

Entering method 'greet'. Hello, my name is Ray. Exiting method 'greet'. 复制代码


装饰器可不仅仅用于方法,还可以用于属性/字段、gettersetter和自动访问器。甚至类本身也可以装饰成子类化和注册。

上面的loggedMethodbound装饰器示例写的很简单,并省略了大量关于类型的细节。实际上,编写装饰器可能相当复杂。例如,上面的loggedMethod类型良好的版本可能看起来像这样:

function loggedMethod<This, Args extends any[], Return>(     target: (this: This, ...args: Args) => Return,     context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return> ) {     const methodName = String(context.name);     function replacementMethod(this: This, ...args: Args): Return {         console.log(`LOG: Entering method '${methodName}'.`)         const result = target.call(this, ...args);         console.log(`LOG: Exiting method '${methodName}'.`)         return result;     }     return replacementMethod; } 复制代码


我们必须使用thisArgsreturn类型参数分别建模this、参数和原始方法的返回类型。

具体定义装饰器函数的复杂程度取决于想要保证什么。需要记住,装饰器的使用次数将超过它们的编写次数,所以类型良好的版本通常是更好的——但显然与可读性有一个权衡,所以请尽量保持简单。

const 类型参数

当推断一个对象的类型时,TypeScript通常会选择一个通用类型。例如,在本例中,names 的推断类型是string[]

type HasNames = { readonly names: string[] }; function getNamesExactly<T extends HasNames>(arg: T): T["names"] {     return arg.names; } // names 的推断类型为 string[] const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]}); 复制代码


通常这样做的目的是实现突变。然而,根据getnames确切的作用以及它的使用方式,通常情况下需要更具体的类型。到目前为止,通常不得不在某些地方添加const,以实现所需的推断:

// 我们想要的类型: readonly ["Alice", "Bob", "Eve"] // 我们得到的类型: string[] const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]}); // 得到想要的类型:readonly ["Alice", "Bob", "Eve"] const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const); 复制代码


这写起来会很麻烦,也很容易忘记。在 TypeScript 5.0 中,可以在类型参数声明中添加const修饰符,从而使类const推断成为默认值:

type HasNames = { names: readonly string[] }; function getNamesExactly<const T extends HasNames>(arg: T): T["names"] { //                       ^^^^^     return arg.names; } // 推断类型:readonly ["Alice", "Bob", "Eve"] // 注意,这里不需要再写 as const const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] }); 复制代码


注意,const修饰符并不排斥可变值,也不需要不可变约束。使用可变类型约束可能会得到意外的结果。例如:

declare function fnBad<const T extends string[]>(args: T): void; // T仍然是string[],因为readonly ["a", "b", "c"]不能赋值给string[] fnBad(["a", "b" ,"c"]); 复制代码


这里,T的推断候选值是readonly ["a", "b", "c"],而readonly数组不能用于需要可变数组的地方。在这种情况下,推理回退到约束,数组被视为string[],调用仍然成功进行。

更好的定义应该使用readonly string[]:

declare function fnGood<const T extends readonly string[]>(args: T): void; // T 是 readonly ["a", "b", "c"] fnGood(["a", "b" ,"c"]); 复制代码


同样,要记住,const修饰符只影响在调用中编写的对象、数组和基本类型表达式的推断,所以不会(或不能)用const修饰的参数将看不到任何行为的变化:

declare function fnGood<const T extends readonly string[]>(args: T): void; const arr = ["a", "b" ,"c"]; //  T 仍然是 string[],const 修饰符没有作用 fnGood(arr); 复制代码


extends 支持多配置文件

当管理多个项目时,通常每个项目的 tsconfig.json 文件都会继承于基础配置。这就是为什么TypeScript支持extends字段,用于从compilerOptions中复制字段。

// packages/front-end/src/tsconfig.json {     "extends": "../../../tsconfig.base.json",     "compilerOptions": {         "outDir": "../lib",         // ...     } } 复制代码


但是,在某些情况下,可能希望从多个配置文件进行扩展。例如,想象一下使用一个TypeScript 基本配置文件到 npm。如果想让所有的项目也使用npm中@tsconfig/strictest包中的选项,那么有一个简单的解决方案:将tsconfig.base.json扩展到@tsconfig/strictest

// tsconfig.base.json {     "extends": "@tsconfig/strictest/tsconfig.json",     "compilerOptions": {         // ...     } } 复制代码


这在一定程度上是有效的。 如果有任何项目不想使用 @tsconfig/strictest,就必须手动禁用这些选项,或者创建一个不从 @tsconfig/strictest 扩展的单独版本的 tsconfig.base.json

为了提供更多的灵活性,Typescript 5.0  允许extends字段接收多个项。例如,在这个配置文件中:

{     "extends": ["a", "b", "c"],     "compilerOptions": {         // ...     } } 复制代码


这样写有点像直接扩展 c,其中 c 扩展 b,b 扩展 a。 如果任何字段“冲突”,则后一个项生效。

所以在下面的例子中,strictNullChecksnoImplicitAny 都会在最终的 tsconfig.json 中启用。

// tsconfig1.json {     "compilerOptions": {         "strictNullChecks": true     } } // tsconfig2.json {     "compilerOptions": {         "noImplicitAny": true     } } // tsconfig.json {     "extends": ["./tsconfig1.json", "./tsconfig2.json"],     "files": ["./index.ts"] } 复制代码


可以用下面的方式重写最上面的例子:

// packages/front-end/src/tsconfig.json {     "extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],     "compilerOptions": {         "outDir": "../lib",         // ...     } } 复制代码


所有枚举都是联合枚举

当 TypeScript 最初引入枚举时,它只不过是一组具有相同类型的数值常量:

enum E {     Foo = 10,     Bar = 20, } 复制代码


E.Foo 和 E.Bar 唯一的特别之处在于它们可以分配给任何期望类型 E 的东西。除此之外,它们只是数字。

function takeValue(e: E) {} takeValue(E.Foo); // ✅ takeValue(123);   // ❌ 复制代码


直到 TypeScript 2.0 引入了枚举字面量类型,它赋予每个枚举成员自己的类型,并将枚举本身转换为每个成员类型的联合。它还允许我们只引用枚举类型的一个子集,并缩小这些类型。

// Color就像是一个联合:Red | Orange | Yellow | Green | Blue | Violet enum Color {     Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet } // 每个枚举成员都有自己的类型,可以引用 type PrimaryColor = Color.Red | Color.Green | Color.Blue; function isPrimaryColor(c: Color): c is PrimaryColor {     // 缩小字面量类型可以捕获bug // TypeScript在这里会报错,因为 // 最终会比较 Color.Red 和 Color.Green。 // 本想使用||,但不小心写了&&     return c === Color.Red && c === Color.Green && c === Color.Blue; } 复制代码


给每个枚举成员指定自己的类型有一个问题,即这些类型在某种程度上与成员的实际值相关联。在某些情况下,这个值是不可能计算出来的——例如,枚举成员可以通过函数调用进行初始化。

enum E {     Blah = Math.random() } 复制代码


每当TypeScript遇到这些问题时,它都会悄无声息地退出并使用旧的枚举策略。这意味着要放弃并集和字面量类型的所有优点。

TypeScript 5.0 通过为每个计算成员创建唯一的类型,设法将所有枚举转换为联合枚举。这意味着现在可以缩小所有枚举的范围,并将其成员作为类型引用。

--moduleResolution

TypeScript 4.7 为 --module--moduleResolution 设置引入了 node16 和 nodenext 选项。这些选项的目的是更好地模拟 Node.js 中 ECMAScript 模块的精确查找规则; 然而,这种模式有许多其他工具没有真正执行的限制。

例如,在 Node.js 的 ECMAScript 模块中,任何相对导入都需要包含文件扩展名。

// entry.mjs import * as utils from "./utils";     //  ❌ - 需要包括文件扩展名。 import * as utils from "./utils.mjs"; //  ✅ 复制代码


在Node.js和浏览器中这样做是有原因的——它使文件查找更快,并且更适合原始文件服务器。但对于许多使用打包工具的开发人员来说,node16/nodenext 的设置很麻烦,因为打包工具没有这些限制中的大部分。在某些方面,node解析模式更适合使用打包工具的人。

但在某些方面,原有的 node 解析模式已经过时了。 大多数现代打包工具在 Node.js 中使用 ECMAScript 模块和 CommonJS 查找规则的融合。

为了模拟打包工具是如何工作的,TypeScript 5.0 引入了一个新策略:--moduleResolution bundler

{     "compilerOptions": {         "target": "esnext",         "moduleResolution": "bundler"     } } 复制代码


如果正在使用现代打包工具,如 Vite、esbuild、swc、Webpack、Parcel 或其他实现混合查找策略的打包工具,那么新的 bundler 选项应该非常适合你。

另一方面,如果正在编写一个打算在 npm 上发布的库,使用bundler选项可以隐藏不使用bundler的用户可能出现的兼容性问题。因此,在这些情况下,使用node16nodenext解析选项可能是更好的方法。

自定义解析标志

JavaScript 工具现在可以模拟“混合”解析规则,就像上面描述的打包工具模式一样。 由于工具的支持可能略有不同,TypeScript 5.0 提供了启用或禁用一些功能的方法。

allowImportingTsExtensions

--allowImportingTsExtensions 允许 TypeScript 文件使用特定于 TypeScript 的扩展名(如 .ts.mts.tsx)相互导入。

仅当启用 --noEmit--emitDeclarationOnly 时才允许使用此标志,因为这些导入路径在运行时无法在 JavaScript 输出文件中解析。 这里的期望是解析器(例如打包工具、运行时或其他工具)将使 .ts 文件之间的这些导入正常工作。

resolvePackageJsonExports

--resolvePackageJsonExports 强制 TypeScript 在从 node_modules 中的包中读取时查询 package.json 文件的 exports 字段。

resolvePackageJsonImports

--resolvePackageJsonImports 强制 TypeScript 在从其祖先目录包含 package.json 的文件执行以 # 开头的查找时查询 package.json 文件的 imports 字段。

--moduleResolutionnode16nodenextbundler 选项下,此选项默认为 true。

allowArbitraryExtensions

在 TypeScript 5.0 中,当导入路径以不是已知 JavaScript 或 TypeScript 文件扩展名的扩展名结尾时,编译器将以 {file basename}.d.{extension} 的形式查找该路径的声明文件。例如,如果在打包项目中使用 CSS loader,可能希望为这些样式表编写(或生成)声明文件:

/* app.css */ .cookie-banner {   display: none; } 复制代码


// app.d.css.ts declare const css: {   cookieBanner: string; }; export default css; 复制代码


// App.tsx import styles from "./app.css"; styles.cookieBanner; // string 复制代码


默认情况下,这个导入将引发一个错误,让你知道TypeScript不理解这个文件类型,你的运行时可能不支持导入它。但是,如果已经配置了运行时或打包工具来处理它,则可以使用新--allowArbitraryExtensions编译器选项来抑制错误。

注意,可以通过添加一个名为 app.css.d.ts 而不是 app.d.css.ts 的声明文件通常可以实现类似的效果。然而,这只是通过 Node 对 CommonJS 的 require 解析规则实现的。严格来说,前者被解释为一个名为 app.css.js 的 JavaScript 文件的声明文件。 因为相关文件导入需要在 Node 的 ESM 支持中包含扩展名,所以在我们的例子中,TypeScript 会在 --moduleResolution node16 或 nodenext 下的 ESM 文件中出错。

customConditions

--customConditions 获取当 TypeScript 从 package.json 的 [exports] 或 (nodejs.org/api/package…) 或 imports 字段解析时应该成功的附加的条件列表。这些条件将添加到解析器默认使用的现有条件中。

例如,当此字段在 tsconfig.json 中设置为:

{     "compilerOptions": {         "target": "es2022",         "moduleResolution": "bundler",         "customConditions": ["my-condition"]     } } 复制代码


任何时候在 package.json 中引用 exports 或 imports 字段时,TypeScript 都会考虑名为 my-condition 的条件。

因此,当从具有以下 package.json 的包中导入时:

{     // ...     "exports": {         ".": {             "my-condition": "./foo.mjs",             "node": "./bar.mjs",             "import": "./baz.mjs",             "require": "./biz.mjs"         }     } } 复制代码


TypeScript 将尝试查找与foo.mjs对应的文件。这个字段只有在 node16、nodenext 和--modulerresolution为 bundler 时才有效。

--verbatimModuleSyntax

默认情况下,TypeScript 会执行一些称为导入省略的操作。如果这样写:

import { Car } from "./car"; export function drive(car: Car) {     // ... } 复制代码


TypeScript 检测到只对类型使用导入并完全删除导入。输出 JavaScript 可能是这样的:

export function drive(car) {     // ... } 复制代码


大多数时候这很好,因为如果 Car 不是从 ./car 导出的值,将得到一个运行时错误。但对于某些边界情况,它确实增加了一层复杂性。例如,没有像 import "./car" 这样的语句,即完全放弃了 import,这实际上对有无副作用的模块产生影响。

TypeScript 的 JavaScript emit 策略也有另外几层复杂性——省略导入并不总是由如何使用 import 驱动的,它通常还会参考值的声明方式。所以并不总是很清楚是否像下面这样的代码:

export { Car } from "./car"; 复制代码


如果 Car 是用类之类的东西声明的,那么它可以保存在生成的 JavaScript 文件中。 但是,如果 Car 仅声明为类型别名或接口,则 JavaScript 文件不应导出 Car。

虽然 TypeScript 可能能够根据来自跨文件的信息做出这些发出决策,但并非每个编译器都可以。

imports 和 exports 的类型修饰符在这些情况下会有帮助。我们可以明确指定importexport仅用于类型分析,并且可以在JavaScript文件中使用类型修饰符完全删除。

// 这条语句可以在JS输出中完全删除 import type * as car from "./car"; // 在JS输出中可以删除命名的import/export Car import { type Car } from "./car"; export { type Car } from "./car"; 复制代码


类型修饰符本身并不是很有用——默认情况下,模块省略仍然会删除导入,并且没有强制区分类型和普通导入和导出。 因此 TypeScript 有标志 --importsNotUsedAsValues 以确保使用 type 修饰符,--preserveValueImports 以防止某些模块省略行为,以及 --isolatedModules 以确保 TypeScript 代码适用于不同的编译器。 不幸的是,很难理解这 3 个标志的细节,并且仍然存在一些具有意外行为的边界情况。

TypeScript 5.0 引入了一个名为 --verbatimModuleSyntax 的新选项来简化这种情况。规则要简单得多,任何没有 type 修饰符的导入或导出都会被保留。任何使用 type 修饰符的内容都会被完全删除。

// 完全被删除 import type { A } from "a"; // 重写为 'import { b } from "bcd";' import { b, type c, type d } from "bcd"; // 重写为 'import {} from "xyz";' import { type xyz } from "xyz"; 复制代码


有了这个新选项,所见即所得。不过,当涉及到模块互操作时,这确实有一些影响。 在此标志下,当设置或文件扩展名暗示不同的模块系统时,ECMAScript 导入和导出不会被重写为 require 调用。相反,会得到一个错误。 如果需要生成使用 requiremodule.exports 的代码,则必须使用早于 ES2015 的 TypeScript 模块语法: image.png虽然这是一个限制,但它确实有助于使一些问题更加明显。 例如,忘记在 --module node16 下的 package.json 中设置 type 字段是很常见的。 因此,开发人员会在没有意识到的情况下开始编写 CommonJS 模块而不是 ES 模块,从而给出意外的查找规则和 JavaScript 输出。 这个新标志确保有意使用正在使用的文件类型,因为语法是有意不同的。

因为 --verbatimModuleSyntax 提供了比 --importsNotUsedAsValues--preserveValueImports 更一致的作用,所以这两个现有标志被弃用了。

支持 export type *

当 TypeScript 3.8 引入仅类型导入时,新语法不允许在 export * from "module" 或 export * as ns from "module" 重新导出时使用。 TypeScript 5.0 添加了对这两种形式的支持:

// models/vehicles.ts export class Spaceship {   // ... } // models/index.ts export type * as vehicles from "./vehicles"; // main.ts import { vehicles } from "./models"; function takeASpaceship(s: vehicles.Spaceship) {   //  ✅ } function makeASpaceship() {   return new vehicles.Spaceship();   //         ^^^^^^^^   // vehicles 不能用作值,因为它是使用“export type”导出的。 } 复制代码


JSDoc 支持 @satisfies

TypeScript 4.9 引入了 satisfies 操作符。它确保表达式的类型是兼容的,而不影响类型本身。以下面的代码为例:

interface CompilerOptions {     strict?: boolean;     outDir?: string;     // ... } interface ConfigSettings {     compilerOptions?: CompilerOptions;     extends?: string | string[];     // ... } let myConfigSettings = {     compilerOptions: {         strict: true,         outDir: "../lib",         // ...     },     extends: [         "@tsconfig/strictest/tsconfig.json",         "../../../tsconfig.base.json"     ], } satisfies ConfigSettings; 复制代码


这里,TypeScript 知道 myCompilerOptions.extends 是用数组声明的,因为虽然 satisfies 验证了对象的类型,但它并没有直接将其更改为 CompilerOptions 而丢失信息。所以如果想映射到 extends 上,是可以的。

declare function resolveConfig(configPath: string): CompilerOptions; let inheritedConfigs = myConfigSettings.extends.map(resolveConfig); 复制代码


这对 TypeScript 用户很有帮助,但是很多人使用 TypeScript 来使用 JSDoc 注释对 JavaScript 代码进行类型检查。 这就是为什么 TypeScript 5.0 支持一个名为 @satisfies 的新 JSDoc 标签,它做的事情完全一样。

/** @satisfies */ 可以捕获类型不匹配:

// @ts-check /**  * @typedef CompilerOptions  * @prop {boolean} [strict]  * @prop {string} [outDir]  */ /**  * @satisfies {CompilerOptions}  */ let myCompilerOptions = {     outdir: "../lib", //  ~~~~~~ oops! we meant outDir }; 复制代码


但它会保留表达式的原始类型,允许稍后在代码中更精确地使用值。

// @ts-check /**  * @typedef CompilerOptions  * @prop {boolean} [strict]  * @prop {string} [outDir]  */ /**  * @typedef ConfigSettings  * @prop {CompilerOptions} [compilerOptions]  * @prop {string | string[]} [extends]  */ /**  * @satisfies {ConfigSettings}  */ let myConfigSettings = {     compilerOptions: {         strict: true,         outDir: "../lib",     },     extends: [         "@tsconfig/strictest/tsconfig.json",         "../../../tsconfig.base.json"     ], }; let inheritedConfigs = myConfigSettings.extends.map(resolveConfig); 复制代码


/** @satisfies */ 也可以内嵌在任何带括号的表达式上。 可以这样写 myCompilerOptions

let myConfigSettings = /** @satisfies {ConfigSettings} */ ({     compilerOptions: {         strict: true,         outDir: "../lib",     },     extends: [         "@tsconfig/strictest/tsconfig.json",         "../../../tsconfig.base.json"     ], }); 复制代码


这可能在函数调用时更有意义:

compileCode(/** @satisfies {CompilerOptions} */ ({     // ... })); 复制代码


JSDoc 支持 @overload

在 TypeScript 中,可以为函数指定重载。 重载提供了一种方式,用不同的参数调用一个函数,并返回不同的结果。它可以限制调用者实际使用函数的方式,并优化将返回的结果。

// 重载: function printValue(str: string): void; function printValue(num: number, maxFractionDigits?: number): void; // 实现: function printValue(value: string | number, maximumFractionDigits?: number) {     if (typeof value === "number") {         const formatter = Intl.NumberFormat("en-US", {             maximumFractionDigits,         });         value = formatter.format(value);     }     console.log(value); } 复制代码


这里,printValue 将字符串或数字作为第一个参数。如果它需要一个数字,它可以使用第二个参数来确定可以打印多少个小数位。

TypeScript 5.0 现在允许 JSDoc 使用新的 @overload 标签声明重载。 每个带有 @overload标签的 JSDoc 注释都被视为以下函数声明的不同重载。

// @ts-check /**  * @overload  * @param {string} value  * @return {void}  */ /**  * @overload  * @param {number} value  * @param {number} [maximumFractionDigits]  * @return {void}  */ /**  * @param {string | number} value  * @param {number} [maximumFractionDigits]  */ function printValue(value, maximumFractionDigits) {     if (typeof value === "number") {         const formatter = Intl.NumberFormat("en-US", {             maximumFractionDigits,         });         value = formatter.format(value);     }     console.log(value); } 复制代码


现在,无论是在 TypeScript 还是 JavaScript 文件中编写,TypeScript 都可以让我们知道是否错误地调用了函数。

printValue("hello!"); printValue(123.45); printValue(123.45, 2); printValue("hello!", 123); // ❌ 复制代码


编辑器中不区分大小写的导入排序

在 Visual Studio 和 VS Code 等编辑器中,TypeScript 支持组织和排序导入和导出的体验。 但是,对于列表何时“排序”,通常会有不同的解释。

例如,下面的导入列表是否排序?

import {     Toggle,     freeze,     toBoolean, } from "./utils"; 复制代码


答案可能是“视情况而定”。 如果不关心区分大小写,那么这个列表显然没有排序。 字母 f 出现在 t 和 T 之前。

但在大多数编程语言中,排序默认是比较字符串的字节值。JavaScript 比较字符串的方式意味着“Toggle”总是在“freeze”之前,因为根据 ASCII 字符编码,大写字母在小写字母之前。 所以从这个角度来看,导入列表是已排序的。

TypeScript 之前认为导入列表是已排序的,因为它会做基本的区分大小写的排序。 对于喜欢不区分大小写排序的开发人员,或者使用像 ESLint 这样默认需要不区分大小写排序的工具的开发人员来说,这可能是一个阻碍。

TypeScript 现在默认检测大小写。这意味着 TypeScript 和 ESLint 等工具通常不会就如何最好地对导入进行排序而相互“斗争”。

这些选项最终可能由编辑器配置。目前,它们仍然不稳定且处于试验阶段,现在可以通过在 JSON 选项中使用 typescript.unstable 在 VS Code 中选择加入它们。 以下是可以尝试的所有选项(设置为默认值):

{     "typescript.unstable": {         // Should sorting be case-sensitive? Can be:         // - true         // - false         // - "auto" (auto-detect)         "organizeImportsIgnoreCase": "auto",         // Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:         // - "ordinal"         // - "unicode"         "organizeImportsCollation": "ordinal",         // Under `"organizeImportsCollation": "unicode"`,         // what is the current locale? Can be:         // - [any other locale code]         // - "auto" (use the editor's locale)         "organizeImportsLocale": "en",         // Under `"organizeImportsCollation": "unicode"`,         // should upper-case letters or lower-case letters come first? Can be:         // - false (locale-specific)         // - "upper"         // - "lower"         "organizeImportsCaseFirst": false,         // Under `"organizeImportsCollation": "unicode"`,         // do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:         // - true         // - false         "organizeImportsNumericCollation": true,         // Under `"organizeImportsCollation": "unicode"`,         // do letters with accent marks/diacritics get sorted distinctly         // from their "base" letter (i.e. is é different from e)? Can be         // - true         // - false         "organizeImportsAccentCollation": true     },     "javascript.unstable": {         // same options valid here...     }, } 复制代码


完善 switch/case

在编写 switch 语句时,TypeScript 现在会检测被检查的值何时具有字面量类型。以提供更便利的代码快捷输入: switchCaseSnippets-5-0_1.gif

速度、内存和包大小优化

TypeScript 5.0 在代码结构、数据结构和算法实现中包含许多强大的变化。这些都意味着整个体验应该更快——不仅仅是运行 TypeScript,甚至安装它。

以下是相对于 TypeScript 4.9 在速度和大小方面的优势:

场景时间或大小相对于 TS 4.9
material-ui 构建时间90%
TypeScript 编译器启动时间89%
Playwright 构建时间88%
TypeScript 编译器自构建时间87%
Outlook Web 构建时间82%
VS Code 构建时间80%
TypeScript npm 包大小59%

图表形式: image.png TypeScript 包大小变化: image.png 那为什么会有如此大的提升呢?部分优化细节如下:

首先,将 TypeScript 从命名空间迁移到模块,这样就能够利用现代构建工具来执行优化。重新审视了打包策略并删除一些已弃用的代码,已将 TypeScript 4.9 的 63.8 MB 包大小减少了约 26.4 MB。还通过直接函数调用带来了显著的速度提升。

在将信息序列化为字符串时,执行了一些缓存。 类型显示可能作为错误报告、声明触发、代码补全等的一部分发生,最终可能会相当昂贵。TypeScript 现在缓存了一些常用的机制以在这些操作中重用。

总的来说,预计大多数代码库应该会看到 TypeScript 5.0 的速度提升,并且始终能够重现 10% 到 20% 之间的提升。当然,这将取决于硬件和代码库特性。

其他重大更改和弃用

运行时要求

TypeScript 现在的 target 是 ECMAScript 2018。TypeScript 软件包还将预期的最低引擎版本设置为 12.20。对于 Node.js 用户来说,这意味着 TypeScript 5.0 需要至少Node.js 12.20 或更高版本才能运行。

lib.d.ts 变化

更改 DOM 类型的生成方式可能会对现有代码产生影响。注意,某些属性已从数字转换为数字字面量类型,并且用于剪切、复制和粘贴事件处理的属性和方法已跨接口移动。

API 重大变更

在 TypeScript 5.0 中, 转向了模块,删除了一些不必要的接口,并进行了一些正确性改进。

关系运算符中的禁止隐式强制

如果编写的代码可能导致隐式字符串到数字的强制转换,TypeScript 中的某些操作现在会进行警告:

function func(ns: number | string) {   return ns * 4; // 错误,可能存在隐式强制转换 } 复制代码


在 5.0 中,这也将应用于关系运算符 >、<、<= 和 >=:

function func(ns: number | string) {   return ns > 4; } 复制代码


如果需要这样做,可以使用+显式地将操作数转换为数字:

function func(ns: number | string) {   return +ns > 4; // OK } 复制代码


弃用和默认更改

在 TypeScript 5.0 中,弃用了以下设置和设置值:

  • --target: ES3

  • --out

  • --noImplicitUseStrict

  • --keyofStringsOnly

  • --suppressExcessPropertyErrors

  • --suppressImplicitAnyIndexErrors

  • --noStrictGenericChecks

  • --charset

  • --importsNotUsedAsValues

  • --preserveValueImports

在 TypeScript 5.5 之前,这些配置将继续被允许使用,届时它们将被完全删除,但是,如果正在使用这些设置,将收到警告。 在 TypeScript 5.0 以及未来版本 5.1、5.2、5.3 和 5.4 中,可以指定 "ignoreDeprecations": "5.0" 以消除这些警告。 很快会发布一个 4.9 补丁,允许指定 ignoreDeprecations 以实现更平滑的升级。除了弃用之外,还更改了一些设置以更好地改进 TypeScript 中的跨平台行为。

  • --newLine,控制 JavaScript 文件中发出的行结束符,如果没有指定,过去是根据当前操作系统推断的。我们认为构建应该尽可能确定,Windows 记事本现在支持换行符,所以新的默认设置是 LF。 旧的特定于操作系统的推理行为不再可用。

  • --forceConsistentCasingInFileNames,它确保项目中对相同文件名的所有引用都在大小写中达成一致,现在默认为 true。 这有助于捕获在不区分大小写的文件系统上编写的代码的差异问题。

参考资料



 标签: 暂无标签

讨论这个帖子(0)垃圾回帖将一律封号处理……