banner
布语

布语

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

ShallowRef return type error fix

Origin#

Today while browsing through the issues of Vue, I came across an interesting issue related to a TypeScript type error problem.

In this issue, the main problem raised was about the export type of 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>

Firstly, we can see that we used ref and passed a union type through generics. In this case, the type returned by ref is Ref<{a: 1} | {b: 2}>, which is the expected type. Furthermore, when listening to notShallow, we don't need to use the .value operation.

Secondly, let's take a look at shallowRef, which is used for shallow reactive data. Similarly, we pass a union type through generics, and the expected type should be ShallowRef<{a: 1} | {b: 2}>. However, the actual type we get is ShallowRef<{a: 1}> | ShallowRef<{b: 2}>. This affects the usage of some methods provided by Vue. For example, when using watch to listen to the value, we need to use .value in the callback function. This is not the most critical issue. The key point is that this code passes the TypeScript check, but it will cause errors in actual runtime because the .value operation is not needed in real runtime. This is where our attention should focus, on the export type of shallowRef, because we have identified that the error lies here.

Searching#

At first, I didn't understand why this problem occurred. I was like a headless fly, randomly searching inside shallowRef. But by chance, I accidentally removed the extends check in the return type of shallowRef, leaving only ShallowRef. Surprisingly, the problem was fixed. So, my attention turned to the step of the extends check.

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

After searching through the documentation and finding information about the special behavior of extends in TypeScript, I discovered the Distributive Conditional Types.

This rule states that when the extends check is applied to a generic type that is a union type, it will split the union type into individual types and then combine them back into a union type. This behavior seems to perfectly match our example and clearly identifies the root cause of the problem: union types, generics, and extends.

After some thought, I realized that it is almost impossible to eliminate generics and extends, as it would result in the loss of type inference. So, the only option left is to handle union types. Going back to the description of the Distributive Conditional Types rule, it seems that we just need to prevent TypeScript from treating the type in the extends check as a union type.

Solution#

With a solution in mind, the implementation becomes simple. For example, we can handle it like this:

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

This approach avoids the Distributive Conditional Types rule. Brilliant!

However, in reality, this is the solution I proposed in the issue. Initially, my approach was more complex.

I thought that since it will be affected by the Distributive Conditional Types rule, why not add an additional check after the extends check to handle this situation.

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

Here, the ShallowRefMarker type serves as a marker similar to the one used in the ShallowRef type. This is how I solved the problem by adding this additional layer of check after the extends check.

Conclusion#

Encountering problems is not scary. The more interesting the problem, the more exciting it can be. And solving it can give a sense of accomplishment.

References:

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types

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

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.