[译]TypeScript 4.7 新特性

778 阅读5分钟

一、Node 支持 ESM了

在 TypeScript 4.7 版本中,通过对 module 设置:node16nodenext 增加了此功能

{
  "compilerOptions": {
    "module": "node16"
    //"module": "nodenext" 
  }
}

接下来我们来一起探索新模式带来的新特性

1. 在 package.json 的新配置 type

Node.js 已经支持了在 package.json 中的新配置 type"type" 支持两个值:"module""commonjs"

{
  "name": "my-package",
  "type": "module",
  //"type": "commonjs"
  
  "//": "...",
  "dependencies": {
  }
}

该配置控制 .js 文件被解释为 ES Modules 还是 CommonJS Modules,默认为 CommonJS Modules 当一个文件被认为是一个 ES 模块时,与 CommonJS 相比,有一些不同的特性:

  • 可以正常使用 import/export 语法
  • 可以在顶部使用 await
  • 相对路径需要完整的扩展(应该使用 import "./foo.js" 而不是 import "./foo")
  • 导入的模块解析可能与 node_modules 中的依赖的不同
  • requiremodule 这样的全局变量不能直接使用了
  • 导入 CommonJS 模块需要特殊的方法

用一个例子来深入看看吧:

// ./foo.ts
export function helper () {
  // ...
}

// ./bar.ts
import { helper } from './foo' // 只在 CJS 中生效

这段代码只会在 CommonJS 模块中生效,接下来来看看如何在 ES 模块 中生效

// ./foo.ts
export function helper () {
  // ...
}

// ./bar.ts
import { helper } from './foo.ts' // 在 ESM 和 CJS 中都生效

2. TypeScript 有了新扩展

.mts.cts.d.mts.d.cts

3. CommonJS 的互通性

Node.js 允许 ES 模块导入 CommonJS 模块,就像它们是具有默认导出的 ES 模块一样。

// ./foo.cts
export function helper() {
   console.log("hello world!");
}

// ./bar.mts
import foo from "./foo.cjs";

// prints "hello world!"
foo.helper();

// 当然另一种方式也支持
// ./foo.cts
export function helper() {
   console.log("hello world!");
}

// ./bar.mts
import { helper } from "./foo.cjs";

// prints "hello world!"
helper();

当然,TypeScript 也不能完全将这两种模块进行整合,但是会有智能语法提示。 下面介绍一种特殊的互通性语法:

import foo = require("foo");

在 CommonJS 模块中,这只是归结为 require() 调用,而在 ES 模块中,导入 createRequire 以实现相同的目的。这将使代码在浏览器等运行时(不支持 require())上的可移植性降低,但通常对互通性很有用。反过来,你可以使用以下语法编写上述示例:

// ./foo.cts
export function helper() {
  console.log("hello world!");
}

// ./bar.mts
import foo = require("./foo.cjs");

foo.helper()

如果对互通性感兴趣的话,可以在这里了解到更多关于互通性相关的知识

4. package.json Exports、Imports 和 自引用

Node.js 支持在 package.json 中定义入口点的新字段,称为“exports”。这个字段是在 **package.json **中定义 "main" 的更强大的替代方法,并且可以控制你的包的哪些部分暴露给用户。 🌰 package.json,它支持 CommonJS 和 ESM 的单独入口点:

{
  "name": "my-package",
  "type": "module",
  "exports": {
    ".": {
      // Entry-point for `import "my-package"` in ESM
      "import": "./esm/index.js",
      
      // Entry-point for `require("my-package") in CJS
      "require": "./commonjs/index.cjs",
    },
  },
  
  // CJS fall-back for older versions of Node.js
  "main": "./commonjs/index.cjs",
}

关于这个新特性的更多信息,你可以在这里了解到更多信息。 下面我们来一起关注下 TypeScript 如何支持它的:

{
    "name": "my-package",
    "type": "module",
    "exports": {
        ".": {
            // Entry-point for `import "my-package"` in ESM
            "import": {
                // Where TypeScript will look.
                "types": "./types/esm/index.d.ts",

                // Where Node.js will look.
                "default": "./esm/index.js"
            },
            // Entry-point for `require("my-package") in CJS
            "require": {
                // Where TypeScript will look.
                "types": "./types/commonjs/index.d.cts",

                // Where Node.js will look.
                "default": "./commonjs/index.cjs"
            },
        }
    },

    // Fall-back for older versions of TypeScript
    "types": "./types/index.d.ts",

    // CJS fall-back for older versions of Node.js
    "main": "./commonjs/index.cjs"
}

⚠️注意:"types" 字段必须在 "export"

TypeScript 也以类似的方式支持 package.json 的“imports”字段,通过在相应文件旁边查找声明文件,并支持包自引用。这些功能通常不涉及设置,默认支持。

二、模块检测控制

将模块引入 JavaScript 的一个问题是现有 "script" 代码和新模块代码之间的歧义。模块中的 JavaScript 代码运行方式略有不同,并且具有不同的范围规则,因此工具必须决定每个文件的运行方式。 例如,Node.js 需要以 .mjs 编写模块入口点,或者附近有一个带有 "type":"module" 的 package.json。 而 TypeScript 在文件中找到任何 import 或 export 语句时,它都会将文件视为模块,否则,将假定 .ts .js 文件是作用于全局范围的脚本文件。 可以看出这与 Node.js 的行为不太匹配,其中 package.json 可以更改文件的格式,或者 --jsx 设置 react-jsx,其中任何 JSX 文件都包含对 JSX 工厂的隐式导入。它也不符合现代期望,因为大多数新的 TypeScript 代码都是在考虑模块的情况下编写的。 所以,在新版中增加了一个新字段 moduleDetection 。该字段接受三个值 "auto" (默认), "legacy" (与 4.6 版本相同) 和 "force"

三、花括号内属性的精确分析

当索引键是 String、Symbol、Number 时,TypeScript 4.7 现在缩小了元素访问的类型。例如,采用以下代码:

const key = Symbol(); // 'name' | 1

const numberOrString = Math.random() < 0.5 ? 42 : "hello";

const obj = {
  [key]: numberOrString,
};

if (typeof obj[key] === "string") {
  let str = obj[key].toUpperCase();
}

下面看看旧版会出现什么样的问题: image.png 这是因为旧版 TypeScript 不会判断** [key] **的类型,它只会将 **[key] **的类型推断为 string | number,这也就是为什么造成了这个报错。 新版解决了该问题,并且也可以正确推断出构造函数的属性在末尾是否已经初始化,话不多说,上例子:

// 'key' has type 'unique symbol'
const key = Symbol();

class C {
  [key]: string; // Property '[key]' has no initializer and is not definitely assigned in the constructor.

  constructor(str: string) {
    // oops, forgot to set 'this[key]'
  }

  screamString() {
    return this[key].toUpperCase();
  }
}

上述示例在旧版中不会报错,感兴趣的可以自己动手试试~

四、改进了对象和方法中的函数推断

TypeScript 4.7 现在可以从对象和数组中的函数执行更精细的推断。这允许这些函数的类型以从左到右的方式始终如一地流动,就像普通参数一样。

declare function f<T>(arg: {
    produce: (n: string) => T,
    consume: (x: T) => void }
): void;

// Works
f({
    produce: () => "hello",
    consume: x => x.toLowerCase()
});

// Works
f({
    produce: (n: string) => n,
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    produce: n => n,
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    produce: function () { return "hello"; },
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    produce() { return "hello" },
    consume: x => x.toLowerCase(),
});

五、实例化表达式

废话不多说,直接看例子:

interface Box<T> {
    value: T;
}

function makeBox<T>(value: T) {
    return { value };
}

// V4.6
function makeHammerBox(hammer: Hammer) {
    return makeBox(hammer);
}
// or...
const makeWrenchBox: (wrench: Wrench) => Box<Wrench> = makeBox;

// V4.7
const makeHammerBox = makeBox<Hammer>;
const makeWrenchBox = makeBox<Wrench>;
const makeStringBox = makeBox<string>;
// TypeScript correctly rejects this.
makeStringBox(42);

六、extends 可以在 infer 中使用了

废话不多说,直接看例子:

// V4.6
type FirstIfString<T> =
    T extends [infer S, ...unknown[]]
        ? S extends string ? S : never
        : never;

// V4.7
type FirstIfString<T> =
    T extends [infer S extends string, ...unknown[]]
        ? S
        : never;

七、新属性 moduleSuffixes

TypeScript 4.7 现在支持 moduleSuffixes 选项来自定义如何查找模块说明符。

{
  "compilerOptions": {
    "moduleSuffixes": [".ios", ".native", ""]
  }
}

如果我们增加了该配置

import * as bar from './foo'

TypeScript 现在将尝试查看相关文件 ./foo.ios.ts./foo.native.ts,最后是 ./foo.ts。 ⚠️注意:moduleSuffixes 中的空字符串 "" 是 TypeScript 查找 ./foo.ts 所必需的。从某种意义上说,moduleSuffixes 的默认值是 [""]

八、resolution-mode

使用 Node 的 ECMAScript 解析,包含文件的模式和您使用的语法决定了如何解析导入;但是,从 ECMAScript 模块中引用 CommonJS 模块的类型会很有用,反之亦然。 TypeScript 现在允许 **/// **指令。

/// <reference types="pkg" resolution-mode="require" />

// or

/// <reference types="pkg" resolution-mode="import" />

九、Go to Source Definition

go-to-source-definition-4-7-v1.gif

总结

TypeScript 4.7 主要是对 Node.js 中模块功能的支持,还有一些是开源贡献者的PR。你觉得本次更新最重要的是什么呢?欢迎评论区讨论~