TypeScript 5.0 Beta 来了

今天,我们很高兴地宣布 TypeScript 5.0 的测试版发布!

此版本带来了许多新功能,同时旨在使 TypeScript 更小、更简单、更快。 我们已经实现了新的装饰器标准、更好地支持 Node 和捆绑器中的 ESM 项目的功能、库作者控制泛型推理的新方法、扩展了我们的 JSDoc 功能、简化了配置,并进行了许多其他改进。

虽然 5.0 版本包括正确性更改和对较少使用的标志的弃用,但我们相信大多数用户将拥有与以前版本类似的升级体验。

要开始使用测试版,您可以通过 NuGet 获取它,或通过以下命令使用 npm:

npm install typescript@beta

装饰器

装饰器是即将推出的 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输出一些以帮助调试问候语。

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. 记录一条“输入(Entering)……”的消息
  2. 将this及其所有参数传递给原始方法
  3. 记录“退出(Exiting)……”消息
  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();

// Output:
//
//   LOG: Entering method.
//   Hello, my name is Ray.
//   LOG: Exiting method.

我们只是在greet上面使用了loggedMethod作为装饰器——注意,我们把它写成了@loggedMethod。当我们这样做时,它会被target方法和一个context对象调用。因为loggedMethod返回了一个新函数,所以这个函数替换了greet的原始定义。

loggedMethod定义了第二个参数。它被称为“上下文对象”,它有一些关于修饰方法是如何声明的有用信息——比如它是#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;
}

我们现在使用context参数——它是loggedMethod中第一个类型比any和any[]更严格的参数。TypeScript提供了一个名为ClassMethodDecoratorContext的类型,它为方法装饰器接受的上下文对象建模。

除了元数据,方法的上下文对象还有一个有用的函数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;

// Works!
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();

// Output:
//
//    Entering method 'greet'.
//   Hello, my name is Ray.
//    Exiting method 'greet'.

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

有关所涉及更改的更多信息,您可以查看原始的pull request。

与实验性遗留装饰器的差异

如果你已经使用TypeScript一段时间了,你可能会意识到它多年来一直支持“实验性”装饰器。虽然这些实验性装饰器非常有用,但它们模拟了装饰器提案的一个更旧的版本,并且总是需要一个称为——experimentalDecorators的可选编译器标志。任何在TypeScript中不使用此标志的尝试都会提示错误。

实验性装饰器将在可预见的未来继续存在;但是,如果没有这个标志,装饰器将成为所有新代码的有效语法。在——experimentalDecorators之外,它们将被类型检查并以不同的方式发出。类型检查规则和emit有很大的不同,虽然可以编写装饰器来同时支持新旧装饰器行为,但任何现有的装饰器函数都不太可能这样做。

这个新的装饰器提案与——emitDecoratorMetadata不兼容,而且它不允许装饰参数。未来的ECMAScript提案可能有助于弥合这一差距。

最后说一句:目前,装饰器的提案要求类装饰器在export关键字之后(如果存在的话)。

export @register class Foo {
    // ...
}

export
@Component({
    // ...
})
class Bar {
    // ...
}

TypeScript会在JavaScript文件中强制执行此限制,但TypeScript文件不会这样做。这部分是由现有用户驱动的——我们希望在我们原始的“实验性”装饰器和标准化装饰器之间提供一个稍微简单的迁移路径。此外,我们从许多用户那里听到了对原始样式的偏好,我们希望在未来的标准讨论中真诚地讨论这个问题。

编写类型良好的装饰器

上面的loggedMethod和bound装饰器示例很简单,并省略了大量关于类型的细节。

输入装饰器可能相当复杂。例如,上面的loggedMethod类型良好的版本可能看起来像这样:

function loggedMethod(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext 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;
}

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

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


constType参数

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

type HasNames = { readonly names: string[] };
function getNamesExactly(arg: T): T["names"] {
    return arg.names;
}

// Inferred type: string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});

通常这样做的目的是实现突变。

然而,根据getnames确切的作用以及它的使用方式,通常情况下需要更具体的类型。

到目前为止,API作者通常不得不建议在某些地方添加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(arg: T): T["names"] {
//                       ^^^^^
    return arg.names;
}

// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

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

declare function fnBad(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(args: T): void;

// T is readonly ["a", "b", "c"]
fnGood(["a", "b" ,"c"]);

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

declare function fnGood(args: T): void;
const arr = ["a", "b" ,"c"];

// 'T' is still 'string[]'-- the 'const' modifier has no effect here
fnGood(arr);


支持多个配置文件

当管理多个项目时,从“基础”配置文件tsconfig.Json继承会很有帮助。使用extends字段,用于从compilerOptions中复制字段。

// packages/front-end/src/tsconfig.json
{
    "compilerOptions": {
        "extends": "../../../tsconfig.base.json",

        "outDir": "../lib",
        // ...
    }
}

然而,在某些情况下,您可能希望从多个配置文件扩展。例如,想象一下使用npm中的TypeScript基础配置文件。如果你希望所有项目都使用npm上@tsconfig/strictest包中的选项,那么有一个简单的解决方案:从@tsconfig/strictest扩展tsconfig.base.json:

// tsconfig.base.json
{
    "compilerOptions": {
        "extends": "@tsconfig/strictest/tsconfig.json",

        // ...
    }
}

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

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

{
    "compilerOptions": {
        "extends": ["a", "b", "c"]
    }
}

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

因此,在下面的例子中,strictNullChecks和noImplicitAny都在最终的tsconfig.json文件中启用了。

// tsconfig1.json
{
    "compilerOptions": {
        "strictNullChecks": true
    }
}

// tsconfig2.json
{
    "compilerOptions": {
        "noImplicitAny": true
    }
}

// tsconfig.json
{
    "compilerOptions": {
        "extends": ["./tsconfig1.json", "./tsconfig2.json"]
    },
    "files": ["./index.ts"]
}

作为另一个例子,我们可以用下面的方式重写原来的。

// packages/front-end/src/tsconfig.json
{
    "compilerOptions": {
        "extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],

        "outDir": "../lib",
        // ...
    }
}

有关更多细节,请阅读原始pull request的更多信息。

所有枚举都是Union枚举

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

enum E {
    Foo = 10,
    Bar = 20,
}

E.Foo和E.Bar唯一的特别之处在于它们可以赋值给任何期望类型为E的东西。除此之外,它们基本上都是数字。

function takeValue(e: E) {}

takeValue(E.Foo); // works
takeValue(123);   // error!

直到TypeScript 2.0引入了枚举字面量类型,枚举才变得更加特殊。Enum字面量类型为每个枚举成员指定了自己的类型,并将枚举本身转换为每个成员类型的并集。它们还允许我们只引用枚举类型的一个子集,并缩小这些类型的范围。


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. Color `。从` Red `到` Color.Green `。
//我们本想使用||,但不小心写了&&
    return c === Color.Red && c === Color.Green && c === Color.Blue;
}

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

enum E {
    Blah = Math.random()
}

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

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

有关此更改的更多详细信息,请参阅GitHub上的详细说明。

——moduleResolution打包

TypeScript 4.7为其——module和——modulerresolution设置引入了node16和nodenext选项。这些选项的目的是为了更好地在Node.js中为ECMAScript模块建立精确的查找规则模型。然而,这种模式有许多其他工具没有真正实施的限制。

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

// entry.mjs
import * as utils from "./utils";     //  wrong - 需要包含文件扩展名

import * as utils from "./utils.mjs"; //  works

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

但在某些方面,最初的node解析模式已经过时了。大多数现代打包工具融合了Node.js中的ECMAScript模块和CommonJS查找规则。例如,无扩展导入就像在CommonJS中一样正常工作,但当查看包的导出条件时,他们会更喜欢ECMAScript文件中的导入条件。

为了建模打包的工作方式,TypeScript现在引入了一个新的策略:——modulerresolution打包。

{
    "compilerOptions": {
        "target": "esnext",
        "moduleResolution": "bundler"
    }
}

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

要了解有关——modulerresolution打包工具的更多信息,请查看实现的拉取请求。

解析自定义标志

JavaScript工具现在可以对“混合”解析规则建模,就像我们上面描述的打包模式一样。由于工具的支持可能略有不同,TypeScript 5.0提供了启用或禁用一些功能的方法,这些功能可能与您的配置一起使用,也可能无法使用。

allowImportingTsExtensions

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

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

resolvePackageJsonExports

——resolvePackageJsonExports强制TypeScript查询package的exports字段。如果它从node_modules中的包中读取数据,则会读取Json文件。

在——moduleResolution的node16、nodenext和bundler选项中,这个选项默认为true。

resolvePackageJsonImports

——resolvePackageJsonImports强制TypeScript查询package的imports字段。当查找以#开头的文件时,该文件的祖先目录包含package.json文件。

在——moduleResolution的node16、nodenext和bundler选项中,这个选项默认为true。

allowArbitraryExtensions

在TypeScript 5.0中,当导入路径以一个不是已知的JavaScript或TypeScript文件扩展名的扩展名结束时,编译器将查找该路径形式为{file basename}.d.{extension}.ts的声明文件。例如,如果你在打包项目中使用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文件中出错。

有关更多信息,请阅读此功能的建议及其相应的pull request。

customConditions

——customConditions接受额外的条件列表,当TypeScript从package.json的[exports]或(https://nodejs.org/api/packages.html#exports)或imports字段解析时,这些条件应该成功。这些条件将添加到解析器默认使用的任何现有条件中。

例如,当在tsconfig. conf中设置此字段时。Json格式如下:

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

在包中引用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和bundler选项下——modulerresolution才有效

--verbatimModuleSyntax

默认情况下,TypeScript会执行导入省略(import elision)操作。基本上,如果你写

import { Car } from "./car";

export function drive(car: Car) {
    // ...
}

TypeScript检测到你只使用了类型导入,并完全删除导入。输出的JavaScript代码可能类似于下面这样:

export function drive(car) {
    // ...
}

大多数情况下,这是很好的,因为如果Car不是从./ Car导出的值,我们会得到一个运行时错误。

但对于某些边界情况,它确实增加了一层复杂性。例如,没有import "./car"这样的语句;-完全放弃导入。这实际上对有没有副作用的模块有影响。

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

export { Car } from "./car";

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

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

导入和导出的类型修饰符在这些情况下有一点帮助。我们可以明确指定import或export仅用于类型分析,并且可以在JavaScript文件中使用类型修饰符完全删除。

//这条语句可以在JS输出中完全删除
import type * as car from "./car";

// 在JS输出中可以去掉命名的import/export ` Car `
import { type Car } from "./car";
export { type Car } from "./car";

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

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

// 完全抹去
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的import和export函数不会被重写。相反,你会得到一个错误。如果你需要发出使用require和module的代码。导出时,你必须使用ES2015之前的TypeScript模块语法:

虽然这是一个限制,但它确实有助于使一些问题更加明显。例如,在package.json中忘记设置type字段是很常见。因此,开发人员会在不知不觉中开始编写CommonJS模块,而不是ES模块,给出令人惊讶的查找规则和JavaScript输出。这个新标志确保你有意使用不同的文件类型,因为它们的语法是不同的。

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

支持foreexport类型*

当TypeScript 3.8引入纯类型导入时,新语法不允许从“module”导出*或从“module”重新导出ns时导出。TypeScript 5.0增加了对这两种形式的支持:

// models/vehicles.ts
export class Spaceship {
  // ...
}

// models/index.ts
export type * as vehicles from "./spaceship";

// main.ts
import { vehicles } from "./models";

function takeASpaceship(s: vehicles.Spaceship) {
  //  ok -`vehicles`只在type位置使用
}

function makeASpaceship() {
  return new vehicles.Spaceship();
  //         ^^^^^^^^
  // ` vehicles `不能用作值,因为它是使用` export type `导出的。
}


JSDoc中的@satisfiesSupport

TypeScript 4.9引入了satisfaction操作符。它确保表达式的类型是兼容的,而不会影响类型本身。例如,让我们看看下面的代码:

interface CompilerOptions {
    strict?: boolean;
    outDir?: string;
    // ...

    extends?: string | string[];
}

declare function resolveConfig(configPath: string): CompilerOptions;

let myCompilerOptions = {
    strict: true,
    outDir: "../lib",
    // ...

    extends: [
        "@tsconfig/strictest/tsconfig.json",
        "../../../tsconfig.base.json"
    ],

} satisfies CompilerOptions;

在这里,TypeScript知道myCompilerOptions.extends是用数组声明的——因为while满足验证了对象的类型,它不会直接将其更改为CompilerOptions并丢失信息。如果我们想映射到extends,没问题。

let inheritedConfigs = myCompilerOptions.extends.map(resolveConfig);

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

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

// @ts-check

/**
 * @typedef CompilerOptions
 * @prop {boolean} [strict]
 * @prop {string} [outDir]
 * @prop {string | string[]} [extends]
 */

/**
 * @satisfies {CompilerOptions}
 */
let myCompilerOptions = {
    outdir: "../lib",
//  ~~~~~~ oops! we meant outDir
};

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

// @ts-check

/**
 * @typedef CompilerOptions
 * @prop {boolean} [strict]
 * @prop {string} [outDir]
 * @prop {string | string[]} [extends]
 */

/**
 * @satisfies {CompilerOptions}
 */
let myCompilerOptions = {
    strict: true,
    outDir: "../lib",
    extends: [
        "@tsconfig/strictest/tsconfig.json",
        "../../../tsconfig.base.json"
    ],
};

let inheritedConfigs = myCompilerOptions.extends.map(resolveConfig);

/** @satisfies */ 也可以在任何括号表达式中使用。我们可以这样写myCompilerOptions:

let myCompilerOptions = /** @satisfies {CompilerOptions} */ ({
    strict: true,
    outDir: "../lib",
    extends: [
        "@tsconfig/strictest/tsconfig.json",
        "../../../tsconfig.base.json"
    ],
});

为什么?好吧,当你深入到其他代码中时,比如函数调用,它通常更有意义。

compileCode(/** @satisfies {CompilerOptions} */ ({
    // ...
}));

此功能由Oleksandr Tarasiuk提供!

JSDoc中的@overloadSupport

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

// Our overloads:
function printValue(str: string): void;
function printValue(num: number, maxFractionDigits?: number): void;

// Our implementation:
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都可以让我们知道我们是否错误地调用了函数。

// all allowed
printValue("hello!");
printValue(123.45);
printValue(123.45, 2);

printValue("hello!", 123); // error!


在—build下传递特定的标志

TypeScript现在允许在——build模式下传递以下标志

这使得您可以更容易地自定义构建的某些部分,其中可能有不同的开发和生产构建。

例如,开发版本的库可能不需要生成声明文件,但生产版本需要。项目可以配置声明发射为默认关闭,并简单地进行构建

tsc --build -p ./my-project-dir

完成内部循环后,“生产”构建只需传递——declaration标志即可。

tsc --build -p ./my-project-dir --declaration


Exhaustiveswitch/caseCompletions

当编写switch语句时,TypeScript现在会检测被检查的值何时具有文字类型。如果是这样,它将为每个case提供一个完整的框架。

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

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

相对于TypeScript 4.9,我们在速度和大小方面取得了一些有趣的胜利。

场景

相对于 TS 4.9时间或者大小

Material-ui构建时间

90%

Playwright 构建时间

89%

tsc 启动时间

89%

tsc 构建时间

86%

Outlook 构建时间

83%

VS Code 构建时间

81%

typescript 打包大小

58%

换句话说,我们发现TypeScript 5.0 Beta版构建VS Code所花费的时间仅为TypeScript 4.9的81%。

如何?有一些值得注意的改进,我们希望在未来提供更多的细节。但我们不会让你等待那篇博客文章。

首先,我们最近将TypeScript从命名空间迁移到模块,允许我们利用现代构建工具来执行作用域提升等优化。使用这个工具,重新审视我们的打包策略,并移除一些废弃的代码,已经从TypeScript 4.9的63.8 MB包大小中削减了约26.5 MB。通过直接调用函数,它也显著地提高了速度。

TypeScript还为编译器中的内部对象类型增加了更多的一致性,同时也精简了某些对象类型。这减少了多态和大态的使用站点,同时抵消了一些作为权衡而来的内存占用。

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

总的来说,我们期望大多数代码库从TypeScript 5.0开始速度都将得到提升,并始终能够重现10%到20%的优势。当然,这取决于硬件和代码库的特性,但我们鼓励您今天就在代码库中尝试一下!


运行时的要求

TypeScript现在以ECMAScript 2018为目标。对于Node用户来说,这意味着最低版本要求至少是Node.js 10及更高版本。

lib.d.ts 变更

DOM类型生成方式的改变可能会对现有代码产生影响。值得注意的是,某些属性已经从数值类型转换为数值字面量类型,处理cut、copy和paste事件的属性和方法也在不同接口之间进行了移动。

API重大变化

在TypeScript 5.0中,我们转向了模块,删除了一些不必要的接口,并对正确性进行了一些改进。有关更改的更多详细信息,请参阅我们的API重大更改页面。

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

如果你编写的代码可能导致隐式的字符串到数字转换,TypeScript中的某些操作将会警告你:

function func(ns: number | string) {
  return ns * 4; // Error, 可能的隐式转换
}

在5.0中,这也适用于关系操作符>、<、<=和>=:

function func(ns: number | string) {
  return ns > 4; //错误
}

如果需要,可以使用+显式强制操作数为数字:

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

这种正确性的改进由Mateusz提供Burzyński。

Enum改革

自TypeScript第一次发布以来,枚举一直存在一些奇怪的问题。在5.0中,我们清理了其中的一些问题,同时减少了理解可以声明的各种枚举所需的概念数量。

你可能会看到两个主要的新错误。第一,将域外字面量赋值给enum类型现在会出错,正如我们所料:

enum SomeEvenDigit {
    Zero = 0,
    Two = 2,
    Four = 4
}

// 错误
let m: SomeEvenDigit = 1;

另一个问题是,声明某种类型的间接字符串/数字混合枚举形式会错误地创建一个全数字枚举:

enum Letters {
    A = "a"
}
enum Numbers {
    one = 1,
    two = Letters.A
}

// Now correctly an error
const t: number = Numbers.two;


在experimentalDecorators下为构造函数中的参数装饰器进行更准确的类型检查

TypeScript 5.0使得——experimentalDecorators下的装饰器的类型检查更加准确。在对构造函数参数使用装饰器时,这一点变得很明显。

export declare const inject:
  (entity: any) =>
    (target: object, key: string | symbol, index?: number) => void;

export class Foo {}

export class C {
    constructor(@inject(Foo) private x: any) {
    }
}

这个调用将失败,因为key需要一个string|symbol,但构造函数参数接收到的键是undefined。正确的修复方法是改变inject中的key类型。如果你使用的库无法升级,一个合理的解决方案是将inject包装在一个更类型安全的装饰器函数中,并对键使用类型断言。


弃用和默认更改

在TypeScript 5.0中,我们已经弃用了以下设置和设置值:

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

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

——forceConsistentCasingInFileNames,这确保了在一个项目中所有对相同文件名的引用都同意使用大小写,现在默认为true。这可以帮助捕获在不区分大小写的文件系统上编写的代码差异问题。

您可以留下反馈并查看关于5.0弃用的跟踪问题的更多信息

接下来是什么?

TypeScript 5.0正在成为一个伟大的版本。在接下来的几周里,我们将专注于bug修复、稳定性和优化即将发布的候选版本,然后是第一个稳定版本。

和往常一样,关于我们的发行版的细节(包括目标日期!)可以在TypeScript 5.0迭代计划中找到。我们希望迭代计划使TypeScript 5.0更容易围绕您和您的团队的时间表进行测试!

我们也希望TypeScript 5.0 Beta版能带来很多你期待已久的新功能。让我们的beta版本(或我们的夜间构建)今天尝试一下,让我们知道你的想法!

展开阅读全文

页面更新:2024-04-23

标签:字段   函数   标志   声明   参数   版本   类型   代码   文件   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top