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 の操作が必要です。さらに重要なことは、このコードは TypeScript を通過しますが、実際の実行ではエラーが発生する可能性があるということです。実際の実行では.value の操作は必要ありません。このため、私たちの焦点は shallowRef のエクスポートされる型に向けられることになりました。

探求#

最初はなぜこのような問題が発生するのか理解できませんでした。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

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。