TypeScript 无法确定类型,即使它具有所有必要的信息

TypeScript does not determine the type even if it has all necessary information

我在 TypeScript 类型中遇到了这个问题。我在同一界面中使用基于不同 属性 的条件类型来确定一个 属性 的类型。这里,FruitBasket 中的 属性 slicerAppleSlicerBananaSlicer 取决于 type.

如果您查看函数 test,它接收 FruitBasket 的实例,可以是 AppleBanana。因此,我通过检查 type 是否等于其中之一来缩小类型,但它仍然抱怨 basket.slicer 不是确定性的。但是,它应该具有确定它是 AppleSlicer 所需的所有信息。我该如何解决这个问题?

我正在使用 TypeScript 4.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()
  }
}

我的首选模式是使切片器适应通用模式,这样您就不必进行此类检查。 (我会在几分钟内更新)不过,你的问题的答案是有一个明确的类型保护来正确检查类型,这将 return 向类型系统确认它确实符合你的条件是特定类型。

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 (isAppleBasket(basket)) {
   // 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()
  }
  else if (isBananaBasket(basket)) {
    basket.slicer.banana()
  }
}

function isAppleBasket(candidate: FruitBasket<Fruits>): candidate is FruitBasket<Fruits.Apple> {
    return candidate.type === Fruits.Apple;
}
function isBananaBasket(candidate: FruitBasket<Fruits>): candidate is FruitBasket<Fruits.Banana> {
    return candidate.type === Fruits.Banana;
}

更新

所以事实证明,转换现有代码并不像我想象的那么容易,因为从枚举扩展与 class 或接口的工作方式不同。这是我更新的例子,有一些解释。如果您想使用 classes 或带有类型的普通 javascript 对象,则由您决定。如果只是为了能够使用 instanceof.

,我更喜欢 classes

这不一定会向您展示我提到的适配器模式(将您的 interface/class 与另一个包装在一起以使它们具有相同的功能集),但它显示了如果您这样做会发生什么(FruitSlicer<T extends Fruit>)

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

interface Fruit {
    fruitType: Fruits;
}

class Apple implements Fruit {
    public fruitType: Fruits = Fruits.Apple;
}
class Banana implements Fruit {
    public fruitType: Fruits = Fruits.Banana;
}

interface SlicedFruit {
    fruitType: Fruits;
    numberOfSlices: number;
}
interface FruitSlicer<T extends Fruit> {
    fruitType: Fruits;
    canHandleFruit(fruit: Fruit): boolean;
    slice(fruit: T): SlicedFruit;
}

class AppleSlicer implements FruitSlicer<Apple> { 
    public fruitType = Fruits.Apple;

    public canHandleFruit(fruit: Fruit): boolean {
        return fruit.fruitType === this.fruitType;
    }

    public slice(fruit: Apple): SlicedFruit{
        return {
            // if we turn the number of slices into a property,
            // we could make an abstract base class that does this work for us
            // for any future fruit.
            fruitType: fruit.fruitType,
            numberOfSlices: 8
        }
    }
}

class BananaSlicer implements FruitSlicer<Banana> { 
    public fruitType = Fruits.Banana;

    public canHandleFruit(fruit: Fruit): boolean {
        return fruit.fruitType === this.fruitType;
    }

    public slice(fruit: Banana): SlicedFruit {
        return {
            fruitType: Fruits.Banana,
            numberOfSlices: 15
        }
    }
}

const allSlicers: FruitSlicer<Fruit>[] = [ new AppleSlicer(), new BananaSlicer() ]

// in terms of objects, a basket can hold several types of fruit. We could make a basket of
// all bananas or all apples, but it doesn't make sense to have ONLY that option.
// this example has a full basket of any fruit type, and we decide for each fruit
//  what type of slicer to use later on.
const basket: Fruit[] = [ new Apple(), new Banana(), new Banana(), new Apple()];

basket.forEach(f => {
    // allSlicers.find could make use of the factory pattern, 
    //      and could be more performant with a map lookup, etc.
    const slicedResult: SlicedFruit | undefined = allSlicers.find(s => s.canHandleFruit(f))?.slice(f);
    console.log(!slicedResult ? 'null' : `${slicedResult?.fruitType} sliced ${ slicedResult.numberOfSlices } ways!`);
})

您可以编写一个type predicates函数来在使用篮子之前检查篮子的类型。

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>
}

function checkTypeOfBasket<T extends Fruits>(basket: FruitBasket<Fruits>, type: T): basket is FruitBasket<T> {
  return basket.type === type
}

const test = (basket: FruitBasket<Fruits>) => {
  if (checkTypeOfBasket(basket, Fruits.Apple)) {
    // after checking, typescript understands the basket is an apple basket
    basket.slicer.apple()
  }
}

这里的大问题是 FruitBasket<Fruits> 根本不是一个合适的 discriminated union where you can check a discriminant property (which would be type) to narrow the type of the object. Not only isn't it a discriminated union, it's not even a union。相当于

{ type: Fruits, slicer: AppleSlicer | BananaSlicer }

因此,根据该定义,这是一个完全有效的 FruitBasket<Fruits>

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

仅仅因为 typeFruits.Apple,并不意味着 slicer 将是 FruitSlicer<Fruits.Apple>。所以如果 test() 接受 FruitBasket<Fruits>,那么它接受 whoops:

test(whoops) // no error here either

那个意味着test()的实现真的不能通过查看basket.type安全地得出任何关于basket.slicer的结论。编译器错误是有效的。哎呀


所以你不希望 test 接受 FruitBasket<Fruits>。你想要的是联合类型 FruitBasket<Fruits.Apple> | FruitBasket<Fruts.Banana>,一个判别式联合,其中 type 是判别式 属性.

如果您不想手动写出该类型(例如,您的 enum 中还有很多其他 Fruits),您可以从您的 [= 版本生成此联合36=]如下:

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

我们在这里创建一个 mapped type with a FruitBasket<F> property type for each F in Fruits, and then immediately indexing into 映射类型 Fruits 以获得所需的联合。

现在我们可以将 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

看起来不错。

Playground link to code