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与编程艺术』。

相关推荐
正义的大古31 分钟前
Vue 3 + TypeScript:深入理解组件引用类型
前端·vue.js·typescript
孟陬3 小时前
bun 单元测试问题之 TypeError: First argument must be an Error object
typescript·单元测试·bun
孟陬4 小时前
CRA 项目 create-react-app 请谨慎升级 TypeScript
react.js·typescript
我是火山呀2 天前
React+TypeScript代码注释规范指南
前端·react.js·typescript
泯泷3 天前
Tiptap 深度教程(二):构建你的第一个编辑器
前端·架构·typescript
一枚前端小能手3 天前
🔥 TypeScript高手都在用的4个类型黑科技
前端·typescript
高木的小天才3 天前
HarmonyOS 页面跳转新方案:HMRouter 路由框架全方位使用指南与实践案例
华为·typescript·harmonyos
胡西风_foxww4 天前
TypeScript 元组类型精简知识点
前端·typescript·类型·元祖