TypeScriptは、必要な情報がすべて含まれていても、タイプを判別しません。

2
2022.01.12

私はTypeScriptタイプでこの問題に遭遇しました。同じインターフェイスの異なるプロパティに基づく条件付きの型を使用して、1 つのプロパティの型を決定しています。ここでは、FruitBasketのプロパティslicerAppleSlicerまたはBananaSlicerに応じてtypeのいずれかになります。

関数testを見ると、FruitBasketのインスタンスを受け取ります。だから、私はtypeがそのうちの1つに等しいことを確認することによって型を絞り込みますが、それでもbasket.slicerは決定的ではないと不平を言います。しかし、AppleSlicerであることを判断するために必要なすべての情報が必要です。どうすればこれを修正できますか?

私はTypeScript4.5.4を使用しています。

enum Fruits {
  Apple = "Apple",
  Banana = "Banana",
}

// These interfaces can have completely different shapes as shown.
interface AppleSlicer { apple(): void }
interface BananaSlicer { banana(): void }

export type FruitSlicer<TType> = TType extends typeof Fruits.Apple
  ? AppleSlicer
  : TType extends typeof Fruits.Banana
  ? BananaSlicer
  : never

export interface FruitBasket<TType extends Fruits> {
  type: TType
  slicer: FruitSlicer<TType>
}

const test = (basket: FruitBasket<Fruits>) => {
  if (basket.type === Fruits.Apple) {
   // This gives me a compile error because basket.slicer is either AppleSlicer or BananaSlicer.
   // But, it should have all information it needs to deduce that it can only be an AppleSlicer
    basket.slicer.apple()
  }
}

回答
2
2022.01.13

ここでの大きな問題は 、FruitBasket<Fruits> が適切な 判別共用 体ではなく、判別プロパティ (type)をチェックしてオブジェクトの型を絞り込むことができるということです。差別的な組合ではないだけでなく、 組合 でもありません。これは、に相当します

{ type: Fruits, slicer: AppleSlicer | BananaSlicer }

したがって、その定義に従って完全に有効な FruitBasket<Fruits> があります:

const whoops: FruitBasket<Fruits> = {
  type: Fruits.Apple,
  slicer: { banana() { } }
} // okay

typeFruits.Appleであるからといって、slicerFruitSlicer<Fruits.Apple>になるわけではありません。したがって、test()FruitBasket<Fruits>を受け入れる場合は、whoopsを受け入れます。

test(whoops) // no error here either

つまり、test()の実装はbasket.typeを見て、basket.slicerについて特に何かを安全に結論付けることができないということです。 コンパイラ エラーは有効なエラーです。おっと。


したがって 、testFruitBasket<Fruits>を受け入れることを望まないのです。必要なのは 、FruitBasket<Fruits.Apple> | FruitBasket<Fruts.Banana>が判別プロパティである判別共用体である共用体型 type です。

その型を手動で書き出したくない場合 (たとえば、enumには他にも多くのFruitsがある)、次のようにFruitBasket<T>のバージョンからこの共用体を生成できます。

type FruitBasketUnion = { [F in Fruits]: FruitBasket<F> }[Fruits]
// type FruitBasketUnion = FruitBasket<Fruits.Apple> | FruitBasket<Fruits.Banana>

ここでは、Fruitsの各FFruitBasket<F>プロパティ型を持つマップされた型を作成し、 を使用してそのマップされた型にすぐにインデックスを作成して、目的の和集合を取得します。

test()のパラメータ型を作成し、basket.type === Fruits.Appleのチェックでbasketの型がFruitBasket<Fruits.Apple>に絞り込まれるようにします。

const test = (basket: FruitBasketUnion) => {
  if (basket.type === Fruits.Apple) {
    basket.slicer.apple() // okay
  }
}

これで、test()の実装はエラーなしでコンパイルされます。それはあなたがもうtest(whoops)を呼び出しることができないことを意味する方が良いです:

test(whoops); // error!
//   ~~~~~~
// Argument of type 'FruitBasket<Fruits>' is not assignable to
// parameter of type 'FruitBasketUnion'.

そのため、コンパイラは whoopsを正しく拒否します。 FruitBasket<Fruits.Apple>FruitBasket<Fruits.Banana>を受け入れることを確認しましょう。

test({
  type: Fruits.Banana,
  slicer: { banana() { } }
}); // okay

test({
  type: Fruits.Apple,
  slicer: { apple() { } }
}); // okay

いい感じです。

コードへの遊び場リンク