起源#
今日、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