类型缩小行为不起作用,具体取决于所涉及的类型

Type narrowing behavior not working depending on types involved

提问人:Don McNamara 提问时间:7/8/2023 最后编辑:Don McNamara 更新时间:7/8/2023 访问量:35

问:

我有一些导致编译器错误的类型缩小代码。当我对缩小范围所涉及的类型进行看似微小的更改时,编译器会检查通过。这些类型有些重叠,并且涉及可选属性。

是否有任何有关类型缩小的规则可以解释此行为?

当编译失败时,应该缩小范围的类型似乎没有被缩小 -- 在本例中是 。对成功编译的类型进行更改时,类型为 。PetPet & Dog

我在生产代码中遇到了这个问题,但创建了一个简单的复制品。

我在操场上检查了以前的 TypeScript 版本,行为似乎是一样的,所以我认为这不是新的编译器行为。

interface Pet {
  name: string; // remove this or make it nullable and it compiles.
  isFriendly: boolean; // remove this it compiles.
}

interface Dog {
  name: string; // remove this and it compiles
  ownerName?: string; // make this non-nullable and it compiles
  // isHappy: boolean; // Add any other non-nullable property and it compiles
}

function isDog(value: any): value is Dog {
  return true;
}

// change the type here to Pet | Dog, or Pet & Dog and it compiles
const testAnimal: Pet = {} as any;

if (isDog(testAnimal)) {
  // when the compiler error occurs, the type of `testAnimal` is `Pet` here, not `Dog` and not `Pet & Dog`
  console.log(testAnimal.ownerName); // <-- Compiler error here: Property 'ownerName' does not exist on type 'Pet'
}

这是指向 Typescript Playground 上相同代码的链接

TypeScript 类型缩小

评论

1赞 Darryl Noakes 7/8/2023
我的观察和“思考”......似乎没有其他属性将其与 ;可为 null 的不计算在内,因为类型保护不能总是使用它们来区分不同的类型。DogPet
0赞 Darryl Noakes 7/8/2023
就目前而言,这两种类型还不够“可区分”,并且由于您标记为 ,因此它保留了这一点。从 中删除使它成为 的超类型:any 是 ,因此 any 可以是 ;然后可以键入 check。删除 from 使两者可区分,并且类型保护可以更改 的类型。添加不可为 null 的属性也使它们可区分。testAnimalPetisFriendlyPetDogDogPetPetDognamePettestAnimalDog
0赞 Darryl Noakes 7/8/2023
只是想法,我不确定。
0赞 Darryl Noakes 7/8/2023
Re “不够可区分”:类型保护无法确定 '可能 '是否为 ,因为检查的唯一方法是检查是否存在 ,它可能不存在(但该值可能仍是类型)。你不能说没有属性,因为对象被允许具有多余的属性(即,在法律上也可以拥有该属性)。PetDogownerNameDogisFriendlyDog
0赞 Darryl Noakes 7/8/2023
标记为显然有效,因为它是 的子类型(类型保护没有任何变化)。似乎标记为有效,因为 TypeScript 现在知道它可能是其中之一,并假设类型保护将根据可为 null 进行区分(随之而来的是误报的可能性)。testAnimalPet & DogDogtestAnimalPet | DogownerName

答:

1赞 jsejcksn 7/8/2023 #1

用户定义的类型保护函数只能具有缩小范围的谓词返回类型,它们不能扩大或以其他方式任意改变操作数参数的类型。

因此,类型 guard 的谓词必须是输入类型和 的交集,因此:Dog

declare function isDog<T>(value: T): value is T & Dog;

在这里,使用泛型类型参数 for 是合适的,以便保留该类型信息,而无需事先知道它(编译器会推断它)——您甚至可以用另一种更广泛的类型来约束它(根据您的用例——即使是类似的东西也总比没有好)。valueobject

下面是使用您提供的数据的完整示例:

TS 游乐场

function hasOptionalProperty<
  O extends object,
  K extends PropertyKey,
  V,
>(
  obj: O,
  prop: K,
  valueValidatorFn: (value: unknown) => value is V,
): obj is O & Partial<Record<K, V>> {
  return (
    !(prop in obj) ||
    valueValidatorFn((obj as Record<K, unknown>)[prop])
  );
}

interface Pet {
  name: string;
  isFriendly: boolean;
}

interface Dog {
  name: string;
  ownerName?: string;
}

function isDog<T>(value: T): value is T & Dog {
  return (
    typeof value === "object" && value !== null &&
    "name" in value && typeof value.name === "string" &&
    hasOptionalProperty(
      value,
      "ownerName",
      (value): value is string => typeof value === "string",
    )
  );
}

const testAnimal: Pet = { name: "Ani", isFriendly: true };

if (isDog(testAnimal)) {
  console.log(testAnimal.ownerName); // Ok
            //^? const testAnimal: Pet & Dog
}

评论

0赞 jsejcksn 7/8/2023
^@DarrylNoakes我不确定你的意思:基本类型保护已经与完整示例分开。你能澄清一下吗?
0赞 Darryl Noakes 7/8/2023
对不起,几秒钟后我意识到那一行实际上是所有改变的东西,并删除了我的评论。
0赞 Darryl Noakes 7/8/2023
我真的很想听听你对更改属性如何影响它的解释。例如,当按照OP所述进行更改时,两种类型的相对位置“狭隘性”如何变化?我的评论几乎完全基于观察和直觉,对 TS 内部和怪癖和边缘情况有一些了解。
1赞 jsejcksn 7/8/2023
^@DarrylNoakes我不确定我是否理解你的意思,但如果你参考 OP 对两个接口属性的评论,那么我同意这似乎很奇怪——了解源代码控制编译器中类型保护评估的人可能需要回答这个问题。