banner
布语

布语

集中一点, 登峰造极⚡️ 布语、布羽、不语、卜语......
github
twitter

修復ShallowRef返回類型錯誤

缘起#

今天在逛 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

https://github.com/vuejs/vue/pull/12979

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。