一、Node 支持 ESM了
在 TypeScript 4.7 版本中,通过对 module 设置:node16 和 nodenext 增加了此功能
{
"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 中的依赖的不同
- 像 require 和 module 这样的全局变量不能直接使用了
- 导入 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();
}
下面看看旧版会出现什么样的问题:
这是因为旧版 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
总结
TypeScript 4.7 主要是对 Node.js 中模块功能的支持,还有一些是开源贡献者的PR。你觉得本次更新最重要的是什么呢?欢迎评论区讨论~