TS 类型防护带复杂类型

TS type guards with complex type

我遇到了一个有趣的TS类型守卫行为,想问问你是我只能接受还是我做错了什么。我把最小的例子放在一起(嗯,接近最小)。

enum LegType {
  LegA = 'LegA',
  LegB = 'LegB',
}

interface LegA {
  type: LegType.LegA
  foo: string
}

interface LegB {
  type: LegType.LegB
  bar: string
}

type Leg = LegA | LegB

enum PlanType {
  T = 'T',
  C = 'C',
}

interface TPlan {
  type: PlanType.T
  legs: LegA[]
}

interface CPlan {
  type: PlanType.C
  legs: (LegA | LegB)[]
}

type Plan = TPlan | CPlan

const isLegA = (o: Leg): o is LegA => o.type === LegType.LegA

const getFoo = (plan: Plan): string | undefined => {
  // const legs: Leg[] = plan.legs // With this line the code builds with no errors.
  const legs = plan.legs // With this line the code contains error, the type of `legs` is resolved as: LegA[] | (LegA | LegB)[]
  const someLegA = legs.find(isLegA)
  return someLegA?.foo // Property 'foo' does not exist on type 'LegA | LegB'.
}

TS playground

为什么需要添加 :Leg[]?是否可以重写代码以便 TS 自己理解正确的类型是什么?

糟糕,我明白发生了什么,我认为这里唯一合理的答案是做你所做的:将 legs 注释为 Leg[]

所以,library type signature for Array's find() method is overloaded and the overload that accepts a user-defined type guard callback and returns a narrowed element type is generic:

find<S extends T>(
    predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any
  ): S | undefined;

find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;

当您不注释 legs 时,它会得到 union type Array<Leg> | Array<LegA>。这种类型被认为与 Array<Leg> 兼容,但它不会自动将其视为兼容(这可能很好。Array<LegA> 不应该让你 push() 一个 LegB ,但 Array<Leg> 应该)。

这意味着 legs.find 的类型本身就是一个 union 其签名不相同的函数:

type LegsFind = {
  <S extends LegA>(p: (this: void, v: LegA, i: number, o: LegA[]) => v is S, t?: any): S | undefined;
  (p: (v: LegA, i: number, o: LegA[]) => unknown, t?: any): LegA | undefined;
} | {
  <S extends Leg>(p: (this: void, v: Leg, i: number, o: Leg[]) => v is S, t?: any): S | undefined;
  (p: (v: Leg, i: number, o: Leg[]) => unknown, t?: any): Leg | undefined;
};

在 TypeScript 3.3 之前,编译器会直接放弃并告诉您它不知道如何调用这样的函数联合。如果您正在调用 find() 等非变异方法,有人会告诉您将 Array<X> | Array<Y> 更改为 Array<X | Y>,然后您将使用 Leg[] 并继续。


引入了 TypeScript 3.3 improved behavior for calling union types. There are cases when the compiler can take a union of functions and represent it as a single function which takes an intersection of the parameters from each member of the union. You can read more about it in the relevant pull request, microsoft/TypeScript#29011

但是,there are cases when it can't do this:特别是联合的多个分支是重载函数或泛型函数。不幸的是 find(),这就是你所处的情况。看起来编译器 尝试 想出一种方法使这个可调用,但它放弃了通用签名对应于 user-defined-type-guard 缩小并以 "regular" 结束,可能是因为它们彼此兼容:

type LegsFind33 = (p: (v: LegA, i: number, o: Leg[]) => unknown, thisArg?: any) => Leg | undefined;

所以你调用成功了,没有发生缩小,你运行进入了这里的问题。调用函数并集的问题没有完全解决;它的 GitHub 问题 microsoft/TypeScript#7294 已关闭并标记为 "Revisit"。

所以在重新讨论之前,这里的建议是 仍然 对待 Array<X> | Array<Y>Array<X | Y> 如果你调用非变异方法,比如 find()... 使用 Leg[] 并继续前进。


好的,希望对您有所帮助;祝你好运!