缘起#
今天在逛 Vue 的 issues 中看到了个有趣的issue, 是一个有关 TypeScript 的类型错误问题.
在这个 issue 中主要提出了一个问题,就是关于 shallowRef 导出类型问题.
<script setup lang="ts">
import { watch, ref } from "vue";
type TestValues ={a: 1} | {b: 2};
const notShallow = ref<TestValues>({ a: 1 }); // Ref<{a: 1} | {b: 2}>
const shallow = shallowRef<TestValues>({ a: 1 });
// ShallowRef<{a: 1}> | ShallowRef<{b: 2}>
watch(notShallow, (val) => {
console.log("a" in val ? val.a : val.b);
});
watch(shallow, (val) => {
console.log("a" in val.value ? val.value.a : val.value.b);
});
</script>
首先可以看到我们使用了 ref 并且通过泛型传入了一个联合类型,此时我们得到 ref 返回的类型为Ref<{a: 1} | {b: 2}>
, 是一个我们预期的类型,并且之后例如监听 notShallow 也不需要进行.value 操作.
其次我们再来看 shallowRef, 该函数用于浅层响应式数据,我们同样通过泛型传入一个联合类型,此时我们预期的到的类型应该为ShallowRef<{a: 1} | {b: 2}>
, 但是,我们实际得到的类型却是ShallowRef<{a: 1}> | ShallowRef<{b: 2}>
, 这影响到了一些 vue 提供的方法的使用,例如同样是进行 watch 监听,却在回调函数中使用值需要进行.value
操作,这还不是最为关键的,关键这样代码是通过了 Ts, 但是在真实运行中确实会出错的,因为在真实运行中是不需要.value
这个操作的,这时候我们的目光就可以聚焦到 shallowRef 的导出类型了,因为我们明确了错误点是在这里.
寻找#
起初还没 get 到到底是为什么会出现这样的问题,像是无头苍蝇一样只知道在 shallowRef 里面一顿乱找,但是还真是瞎猫碰到了死耗子,在无意之间把 shallowRef 的返回类型中的extends
判断删除了,只保留了ShallowRef<T>
, 恰好问题修复了,于是目光再次聚焦,聚焦到了extends
判断的这一步.
export declare function shallowRef<T extends object>(value: T): T extends Ref ? T : ShallowRef<T>;
经过翻阅资料,查找有关于 Typescript 中extends
的特殊点,查到了相关描述,叫做分配律, 在 Typescript 官方中称呼为Distributive Conditional Types.
这样规则具体描述为当extends
判定对象恰好为泛型
, 并且泛型恰好为联合类型
, 那么就会开启这种行为,这种行为不会直接把联合类型
接着传递,而是会把联合类型
先拆分然后拆分的每一项分别得到一个结果类型,然后再把每一项得到的结果类型拼接起来称为一个联合类型.
这种行为似乎恰好符合了我们上面的例子,也明确了问题的根源: 联合类型
、泛型
、extends
.
思考之后发现,泛型
和extends
基本上是不可能把消除掉的,否则就会丢失类型推导,我们只能入手处理联合类型
了,回到上面的Distributive Conditional Types规则的描述,似乎我们只需要让 TypeScript 不认为前面的判定的类型为联合类型
就可以了.
解决#
有了解决思路那处理起来就简单了,例如这样处理:
export declare function shallowRef<T extends object>(value: T): [T] extends [Ref] ? T : ShallowRef<T>;
机制的避免了Distributive Conditional Types规则,泰酷辣!
但实际上这是提出这个 issue 本人提供的解决方案,我在起初的处理不是这样的,会更加复杂一些.
我在想着既然会被分配律, 那不如我在extends
处理完毕之后在判定一层,套娃一层把这种情况处理掉.
export declare function shallowRef<T extends object>(value: T): (T extends Ref ? T : ShallowRef<T>) extends {
[ShallowRefMarker]?: true;
} ? ShallowRef<T> : T;
其中ShallowRefMarker
类型是作为ShallowRef
类型中标记类似的存在,于是乎我就从这里入手解决掉了这个问题.
总结#
遇到问题并不可怕,问题越有意思,有时候越令人兴奋,解决后也能得到成就感.
参阅
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types