最近遇到了一个有意思的 TypeScript 小问题,让我理解了 TS 在 access 和 assign value 的时候会有不同的类型推断。下面是一个简单的例子:我们现在有一个 object,想要在 runtime 根据 key 的取值来修改对应的 value:
type User = {
name: string;
age: number;
};
const user: User = {
name: "frank",
age: 24,
};
const key = Math.random() < 0.5 ? "name" : "age";
// key has type "name" | "age"
const value = user[key];
// access value: user[key] has type string | number
user[key] = key === "name" ? "felix" : 23; // TS error: Type 'string | number' is not assignable to type 'never'
// assign value: user[key] has type never
这么写从逻辑上看好像没有问题,但是 TS 会对最后一行的 assignemnt 报错说 Type 'string | number' is not assignable to type 'never'
。研究了一下发现同样是推断 user[key]
的类型,但是在 access 和 assign 的情况下,TS 的表现是不同的:
在 access value 时候,TS 会做 union,推断出 user[key]
的类型是 string | number
。这里的表现比较符合直觉,毕竟是有两种可能所以取 union。
在 assign value 的时候,TS 会做 intersection,推断出 user[key]
的类型是 string & number = never
。第一眼会感觉有点反直觉,但其实这里 TS 是想要确保 RHS 可以安全的赋给所有可能的 LHS,所以才为了会找一个跟 LHS 所有 type 都 compatible 的子集而做 intersection。
要解决这个 error 的方法也非常简单:// @ts-ignore
if (key === "name") {
// user[key] has type string
user[key] = "felix";
} else {
// user[key] has type number
user[key] = 23;
}
这样就不会有 error 的原因是 TS 会做 type narrowing,推断出在第一条 branh 上 user[key]
只能是 string
,在第二条 branch 上 user[key]
只能是 number
。
需要注意的是这种 type narrowing 只适用于 explicit control flow,所以这里的 if statement 可以,而一开始的 ternary expression 就不行。