ShallowRef返回类型错误修复

缘起

​ 今天在逛 Vue 的 issues 中看到了个有趣的issue, 是一个有关 TypeScript 的类型错误问题.

​ 在这个 issue 中主要提出了一个问题，就是关于 shallowRef 导出类型问题.

Copy

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);
});


​ 首先可以看到我们使用了 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判断的这一步.

Copy
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 不认为前面的判定的类型为联合类型就可以了.

解决

​ 有了解决思路那处理起来就简单了，例如这样处理:

Copy
export declare function shallowRef<T extends object>(value: T): [T] extends [Ref] ? T : ShallowRef<T>;

​ 机制的避免了Distributive Conditional Types规则，泰酷辣！

​ 但实际上这是提出这个 issue 本人提供的解决方案，我在起初的处理不是这样的，会更加复杂一些.

​ 我在想着既然会被分配律, 那不如我在extends处理完毕之后在判定一层，套娃一层把这种情况处理掉.

Copy
export declare function shallowRef<T extends object>(value: T): (T extends Ref ? T : ShallowRef<T>) extends {
[ShallowRefMarker]?: true;
} ? ShallowRef<T> : T;

​ 其中ShallowRefMarker类型是作为ShallowRef类型中标记类似的存在，于是乎我就从这里入手解决掉了这个问题.

总结

​ 遇到问题并不可怕，问题越有意思，有时候越令人兴奋，解决后也能得到成就感.

参阅