TypeScript 5.8 beta 发布:函数类型推导优化、require ESM 支持、NodeJs TS 支持

首页 编程分享 JQUERY丨JS丨VUE 正文

林不渡 转载 编程分享 2025-02-04 20:13:17

简介 新年快乐! TypeScript 已于 2025.1.29 发布 5.8 beta 版本,你可以在 5.8 Iteration Plan 查看所有被包含的 Issue 与 PR。如果想要抢先体验新特性


新年快乐!

TypeScript 已于 2025.1.29 发布 5.8 beta 版本,你可以在 5.8 Iteration Plan 查看所有被包含的 Issue 与 PR。如果想要抢先体验新特性,执行:

$ npm install typescript@beta

来安装 beta 版本的 TypeScript,或在 VS Code 中安装 JavaScript and TypeScript Nightly ,并选择为项目使用 VS Code 的 TypeScript 版本(cmd + shift + p, 输入 select typescript version),来更新内置的 TypeScript 支持。

本篇是笔者的第 13 篇 TypeScript 更新日志,上一篇是 「TypeScript 5.6 beta 发布:更完善的空值与真值检查、Iterator Helper、使用 --noCheck 跳过类型检查」,你可以在此账号的创作中找到(或在掘金/知乎/Twitter搜索林不渡),接下来笔者也将持续更新 TypeScript 的 DevBlog 相关,感谢你的阅读。

函数类型推导优化

在 TypeScript 中,我们经常遇到函数声明的返回值类型依赖于参数类型的情况,如以下的示例:

enum ValueKind {
    Single,
    Multiple,
}

async function getValue(
    value: ValueKind,
): Promise<string | string[]> {
    if (value === ValueKind.Single) {
        return '';
    }
    else {
        return [];
    }
}

我们希望,getValue 的返回值类型取决于参数 value 的类型。这在逻辑层是很好实现的,但在类型层面则需要一些额外的处理,比如你可能会首先想到重载:

async function getValue(
    value: ValueKind.Single,
): Promise<string>;
async function getValue(
    value: ValueKind.Multiple,
): Promise<string[]>;
async function getValue(
    value: ValueKind,
): Promise<string | string[]> {
    if (value === ValueKind.Single) {
        return '';
    }
    else {
        return [];
    }
}

getValue(ValueKind.Single) // string;
getValue(ValueKind.Multiple) // string[];

然而重载有两个比较显著的问题,首先,它并不会在函数实现内检查每个重载实现的返回值,比如:

if (value === ValueKind.Single) {
-        return '';
+         return [];
}

这里的返回值实际上并不符合我们指定的重载声明,但 TypeScript 目前并不会检查其有效性。

另外一个问题则是,在参数类型-返回值类型对应较多的情况下,密密麻麻的重载会导致函数签名难以阅读,同时也不能很好处理参数类型-返回值类型对应关系需要通过进一步计算得到,而不是直接枚举的情况。

另外一个实现这种情况的方式则是使用条件类型:

type ReturnValue<S extends ValueKind> =
    S extends ValueKind.Multiple ? string[] :
    string;

async function getValue<S extends ValueKind>(
    value: S,
): Promise<ReturnValue<S>> {
    if (value === ValueKind.Single) {
        return ''; // Type 'string' is not assignable to type 'ReturnValue<S>'
    }
    else {
        return []; // Type 'string[]' is not assignable to type 'ReturnValue<S>'
    }
}

但这种使用也会带来新的问题,即如上的报错。这是因为 TypeScript 会直接将返回语句的类型,与函数返回值的类型进行比较,即 string vs S extends ValueKind.Multiple ? string[] : string,而在这个比较过程中,类型参数 S 是还没有被填充的,因此这个类型比较必定不兼容——类似于重载的情况。

你当然可以通过额外的类型断言来修正这一情况,但这样就变成是我们在指导 TypeScript 类型,而不是由它自己来推导类型并确保类型安全了。

很容易想到,既然我们都已经走到能够进行类型收窄的语句内了,如果能对应填充下参数类型,岂不是就解决这个问题了?在 TypeScript 4.8 版本中就支持了这一能力:


type ReturnValue<S extends ValueKind> =
    S extends ValueKind.Multiple ? string[] :
    S extends ValueKind.Single ? string :
    never;

async function getValue<S extends ValueKind>(
    value: S,
): Promise<ReturnValue<S>> {
    if (value === ValueKind.Single) {
        return '';
    }
    else {
        return [];
    }
}

要使用这个能力,我们需要更新一下 ReturnValue 工具类型的实现,让我们预期的所有参数类型的匹配结果都显式标注出来(语句块内对条件类型进行类型收窄时,只会在完全命中一个 extends 条件时生效)。

require ESM 支持

说实话我每次看到 CJS 和 ESM 的『你中有我,我中有你』时都感到一丝蛋疼,估计 Ryan 也没想到这么多年过去了,社区还在打补丁,就好像在努力挽回一段破碎的婚姻一样。

此前我们可以在 ESM 中加载 CJS 模块,但反之则行不通,所以当你必须兼容 CJS 时,又遇到只发布了 ESM 产物的 npm 包,那得是多么头大的情况。而 NodeJS 22 版本实验性质地支持了使用 require 加载 ESM 文件(loading-ecmascript-modules-using-require),能够在 CJS 中加载同步(即不包括 top-level await)的 ESM 文件。

TS 5.8 版本现在能够在启用 --module nodenext 配置时支持这一行为,需要注意的是由于这个功能特性可能会反向移植到更早的 NodeJs 版本,比如 Node 20,因此还不会提供具体的如 --module node18 这样精确的版本控制。

NodeJs TS 支持 --erasableSyntaxOnly

NodeJs 22.6.0 版本开始通过实验性的 --experimental-strip-types 选项支持直接执行 TS 文件,同时在 23.6.0 版本开始默认启用了此配置,也就是说,你可以直接 node index.ts 而不需要额外的命令行参数了,对应的,23.6.0 也新增了 --no-experimental-strip-types 配置来阻止这一默认行为。

NodeJs 底层是通过把类型相关的代码替换为空白字符串来实现此能力的,其底层是 Amaro ,Amaro 底层则是 @swc/wasm-typescript,整体使用大致是这样:

const amaro = require('amaro');

const { code } = amaro.transformSync("const foo: string = 'bar';", { mode: "strip-only" });

console.log(code); // "const foo         = 'bar';"

对于某些不能直接移除的 TS 特性(比如枚举),NodeJs 23.7.0 也支持了 --experimental-transform-types 配置来做执行前的语法转换(通过设置 transformSync options 的 mode 为

'transform'),比如第一个例子中的代码,如果直接执行会有这么个错误:

node:internal/modules/typescript:67
        throw new ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(error.message);
        ^

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]:   x TypeScript enum is not supported in strip-only mode
   ,-[1:1]
 1 | ,-> enum ValueKind {
 2 | |     Single,
 3 | |     Multiple,
 4 | `-> }

如果你在迁移某些工具链到原生的 NodeJs TS 支持,并且希望使用 'strip-only' 模式,那么就必须检查出代码中无法直接 strip 的部分。因此,TypeScript 5.8 引入了 --erasableSyntaxOnly 配置,能够帮你提前检查出这种代码。

先是为 JS 引入类型注释的 TC39 提案 proposal-type-annotations,接着是 NodeJs 支持直接执行 TS 文件,感觉浏览器直接运行 TS 文件也不远了?

变量初始化检查(5.7 补档)

TS 5.7 版本开始支持了对跨函数上下文的未初始化变量检查,如对于以下的例子:

function foo() {
    let result: number
    
    function printResult() {
        console.log(result); // ok before 5.7, error since 5.7
    }
}

TS 5.7 版本只有这个类型能力增强值得一写,所以我就直接把 TS 5.7 的 Devblog 鸽了 :-)

其它

计算属性类型声明优化

此前 TypeScript 会约束 Class 结构内的计算属性,要求其类型必须是字面量类型或 symbol 类型,如:

export let propName = 'theAnswer';

export class MyClass {
  [propName] = 42;
  //  ~~~~~~~~~~
  // error!
  // A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type.
}

这是因为在此前,TS 希望在编译时确定这一计算属性的具体类型,而在这个例子里 propName 是 string 类型,如果换成使用 const 声明,将其推导类型收窄为字符串字面量类型,就没有这个错误了。

虽然有这么个错误,但是 TS 仍然能够编译出声明代码:

export declare let propName: string;

export declare class MyClass {
    [x: string]: number;
}

使用 const 声明编译出的类型声明则是这样的:

export declare const propName = "theAnswer";

export declare class MyClass {
    [propName]: number;
}

看起来好像很合理,但你会发现,为 Class 通过计算属性声明索引类型其实是完全合法的,即以下的代码都是合法且效果一致的:

export declare let propName: string;
export declare class MyClass {
    [propName]: number;
}

export declare let propName: string;
export declare class MyClass {
    [x: string]: number;
}

那么此前限定计算属性必须为字面量类型或 Symbol 类型的限制,其实就不存在了。因此 TypeScript 5.8 版本移除了这一限制,现在使用 let 声明不会抛出错误,且与 const 编译结果一致:

export let propName = 'theAnswer';

export class MyClass {
  [propName] = 42;
}

// let generated
export declare let propName: string;
export declare class MyClass {
    [propName]: number;
}
export const propName = 'theAnswer';

export class MyClass {
  [propName] = 42;
}

// const generated
export declare const propName = "theAnswer";
export declare class MyClass {
    [propName]: number;
}

全文完,我们 TS 5.9 见 :-)

转载链接:https://juejin.cn/post/7465730052421091364


Tags:


本篇评论 —— 揽流光,涤眉霜,清露烈酒一口话苍茫。


    声明:参照站内规则,不文明言论将会删除,谢谢合作。


      最新评论




ABOUT ME

Blogger:袅袅牧童 | Arkin

Ido:PHP攻城狮

WeChat:nnmutong

Email:nnmutong@icloud.com

标签云