TypeScript v5 一个非常有用的新语法:“const 类型参数”

前述文章讲到我们的 CRA(C reate R eact App)项目升级 TypeScript 到 v5 遇到了 TypeScript 版本冲突问题,但是没有讲到为何要进行此次升级,本文介绍原因以及详细介绍 TypeScript v5 新引入的一个语法。

ts-pattern Expected 0 arguments, but got 1

项目引入 ts-pattern 后,需要升级到 TypeScript v5,否则将报错:match doesnt work (Expected 0 arguments, but got 1)

issue 亦有记载 github.com/gvergnaud/t...

因为 ts-pattern 引入了一个新语法:"const 类型参数"const type parameters)。

为何要引入 ts-pattern?

如果大家用过 Rust 或 Python 的 match 一定会被该库吸引,具体介绍可见我的这篇文章 TypeScript 系列:无需再写 if / else 了!引入 Rust / Python 模式匹配到 TS 中

ts 复制代码
export declare function match<const input, output = symbols.unset>(value: input): Match<input, output>;
//                            ^^^^^

const 类型参数即此处的 const input

const 类型参数

这是 TypeScript 5.0 引入的特性。作用:

  • 它会尽可能保持字面量类型的精确性,而不是拓宽为更通用的类型
  • 对于对象和数组,它会推断出只读类型

一句话介绍:const type parameters 让我们的类型能进一步收窄而无需在调用时每次手动增加 as const

为了了解新语法的用处,我们先看看普通泛型写法:

ts 复制代码
declare function f<T>(x: T): T;
const r = f({ a: 1 });   // T 推断为 { a: number },不是 { a: 1 }

我们想进行严格推断,在 TypeScript v4 可以通过 as const 将类型从 number 收窄到字面量 1

ts 复制代码
declare function f2<T>(x: T): T;
const r2 = f2({ a: 1 } as const); // T 推断为 { readonly a: 1 }

但每次调用都得加麻烦且易忘记 😩,v5 可以给入参增加 const 关键词,一劳永逸 💐:

ts 复制代码
declare function f3<const T>(x: T): T;
//                  ^^^^^

const r3 = f3({ a: 1 }); // T 推断为 { readonly a: 1 }

大家可以在 playground 试试 www.typescriptlang.org/play/

官方示例

我们再结合官方示例讲解下,官方示例是 v5.0 的,现在最新版本 v5.8.3 有些许变化也一并会讲到。

当 TypeScript 推断一个对象的类型时,通常会选用一种泛化 的类型。例如,在下面这段代码里,names 的推断类型是 string[]

ts 复制代码
type HasNames = { names: readonly string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
    return arg.names;
}

// 推断类型:string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

这样做的目的通常是为了后续还能对这个数组进行修改。

然而,根据 getNamesExactly 的具体实现及其使用场景,我们往往更希望得到更精确的类型

在 5.0 之前,库作者通常只能建议调用者在某些地方手动加上 as const 来达到期望的推断:

ts 复制代码
// 我们想要的类型:
//    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 修饰符 ,让上述 as const 的推断成为默认行为:

ts 复制代码
type HasNames = { names: readonly string[] }; // 我注:此处的 readonly 加或不加有差别下文会讲到
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 修饰符并不会拒绝可变值 ,也不要求约束必须是不可变类型

如果约束本身是可变类型,结果可能出乎意料。例如:

ts 复制代码
declare function fnBad<const T extends string[]>(args: T): void;
// T 仍旧是 string[],因为 readonly ["a", "b", "c"] 不能赋值给 string[] - 我注:这里有误
fnBad(["a", "b", "c"]);

在这里,推断候选是 readonly ["a", "b", "c"],而只读数组不能赋给可变数组,于是推断回退到约束 string[],调用仍然成功。

我注:上述 fnBad 描述是 v5.0 的,最新版 TS 如果不加 readonly 并不会推断为 string[] 而是 ["a", "b", "c"],二者的区别是后者只能添加字符串 a b c,但仍然是可变 的,因为虽然加了 const 但是没有加 readonly

改一下返回值类型,就容易理解一些了

ts 复制代码
declare function fnBad<const T extends string[]>(args: T): T;
// T 在 v5.0 是 string[],在 v5.8 是 ["a", "b", "c"],因为 readonly ["a", "b", "c"] 不能赋值给 string[]

// v5.8 list 类型 ["a", "b", "c"] 非 readonly ["a", "b", "c"] 故可修改
const list = fnBad(["a", "b", "c"]);

// 但只能 push "a" / "b" / "c",如果是 v5.0 可 push 任意字符串。
list.push('a')

正确的做法是把约束也设成 readonly string[](我注:这句话没错):

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

同样要记住:const 修饰符只影响 在调用处直接写出 的对象、数组或基本类型字面量;对于"事先存好的变量"不会起作用:

ts 复制代码
declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ["a", "b", "c"];
// T 仍是 string[] ------ const 修饰符在此无效
fnGood(arr);

ts-pattern 结合 const type parameters

到这里我们就能理解为何 ts-pattern 需要依赖这个新特性。

ts 复制代码
declare let input: "A" | "B";

return match(input)
  .with("A", () => "It's an A!")
  .with("B", () => "It's a B!")
  .exhaustive();

如果不利用 const 类型参数,ts-pattern 的 with 函数参数就可以是任意字符串,有了 const 类型参数的"加持"才能做到精确推断 以及穷尽性检查

更多阅读

公众号『JavaScript与编程艺术』。

相关推荐
@@@@@@@@5419 小时前
JavaScript 的缺点与 TypeScript 的弥补
开发语言·javascript·typescript
用户09340777351420 小时前
HarmonyOS WPS Open SDK 实践:最小打开闭环与 enableEdit 分支处理
typescript·harmonyos
柯克七七2 天前
我把 bug 修得太干净了,测试组以为这个模块本来就没问题
前端·javascript·typescript
退休倒计时2 天前
【每日一题】LeetCode 39. 组合总和 TypeScript
算法·leetcode·typescript
yyt3630458412 天前
KlineChartQuant Tooltip 高频交互 200FPS 性能优化完整复盘
前端·经验分享·性能优化·typescript·vue·交互·chrome devtools
用户0934077735142 天前
HarmonyOS WPS Open SDK 实践:办公审批附件「打开—编辑—回传」代码实现
typescript·harmonyos
Flynt2 天前
TypeScript 5.8 让我少踩了一个大坑,顺手再聊聊 --erasableSyntaxOnly
前端·javascript·typescript
退休倒计时3 天前
【每日一题】LeetCode 17. 电话号码的字母组合 TypeScript
算法·leetcode·typescript
无情的西瓜皮4 天前
MCP 协议实战:从零搭建一个 MCP 服务器(Node.js 版)
vue.js·人工智能·typescript·node.js
退休倒计时4 天前
【每日一题】LeetCode 78. 子集 TypeScript
算法·leetcode·typescript