区分联合类型

3

为什么我不能只通过成员x是字符串而不是数字的信息来鉴别该联合类型?为什么一定要使用字面量类型?

type A = { x: string ; y: string }
type B = { x: number ; y: number }
type C = A | B

function fn(p: C) {
  if (typeof p.x === 'string') { 
    // Typescript is unable to infer p as A
    // Typescript infer p.y as (string | number), why not just string ?
  }
  // Typescript is capable of inferring p.x as number - good
  // But cannot infer p.y as number, why ?
}

对于我来说,使用像{x:string,y:number}这样的类型调用函数是不可能的,那么为什么Typescript认为这是可能的呢?


这段代码不合逻辑,“typeof { x: string ; y: string }”应该是对象,而不是字符串。 - Mor Shemesh
我编辑了帖子,抱歉。 - Romain Delhomel
3个回答

4

这并不是一个直接解释为什么Typescript编译器无法通过typeof区分联合类型的原因,但根据手册所述:

需要三个要素:

  1. 具有公共单例类型属性(鉴别属性)的类型。
  2. 取这些类型联合的类型别名(联合类型)。
  3. 在共同属性上使用类型守卫。

因此,为了缩小正确类型的范围,需要使用单例类型属性。我会使用自定义类型守卫:

type A = { x: string ; y: string }
type B = { x: number ; y: number }
type C = A | B

function isA(obj: C): obj is A { 
  return typeof obj.x === 'string';
}

function isB(obj: C): obj is B { 
  return typeof obj.x === 'number';
}

function fn(p: C) {

  if (isA(p)) {
    // ...
  }

  if (isB(p)) {
    // ...
  }

}

似乎应该是这样的 function isB(obj: C): obj is B - Codd Wrench

2
如果你可以用类来表达类型,我建议这样做:
class A { x: string; y: string; }
class B { x: number; y: number }
type C = A | B
function fn(p: C) {
    if (p instanceof A) {
        // { x: string; y: string; }
    }
    if (p instanceof B) {
        // { x: number; y: number }
    }

}

在这种情况下,您可以使用 instanceof魔法。这是最好的方法。否则,您需要检查所有属性,因为关键字类型仅向源添加定义。

谢谢提供另一种选择,但我想了解为什么 TypeScript 无法在我的示例中正确推断类型。 - Romain Delhomel
@Romain 因为 typeof 就是这样工作的,而且在 typeof 后面的值应该转换为所需的类型。 - Codd Wrench
typeof允许Typescript推断p.x是数字或字符串,无需转换。但是Typescript在此处停止推断,即使它有推断p.y所需的所有信息。 - Romain Delhomel

0

虽然这个问题已经问了一年多了,但我希望我的演示能够帮助到那些发现这个问题的人。

以下解释摘自《Programming Typescript》,稍作修改以适应此处的示例代码:

isAorB接受一个类型为"AorB"的参数时,并不意味着我们必须传递一个AB——实际上,我们可以传递一个类型为A | B的参数。

type A = { x: string ; y: string };
type B = { x: number; y: number };
type AorB = A | B;

const c = { x: "str", y: "str" };

function isAorB(p: AorB) {
    if (typeof p.x === "number") { 
        const alsoCx = p.x; // number
        const alsoCy = p.y; // string | number
        return;
    }
    const alsoCx = p.x; // string
    const alsoCy = p.y; // string | number
}

isAorB(c);

我承认,即使我盯着这段文字很长时间,我仍然不理解其中的逻辑。

但是,我尝试了很多类型的组合,并且认为Typescript假定以下内容也是可能的输入:

// The above c really satisfy type C here.
type C = { x: string | number ; y: string | number };

尽管不知道Typescript如何解析上述逻辑也许甚至不是一个答案。现在,上述行为对我来说并不那么奇怪。

注意:如果将c定义为const c: C = ...,则在调用isAorB(c)时实际上会遇到错误。

既然您提到了“字面类型”,我假设您知道一个简单的解决方案是在联合对象的所有公共键中添加“唯一字面值”以区分它们。以下是一个简短的示例供那些不知道(因为还没有人使用这种解决方案的样本)的人参考。

使用唯一的字面标签解决模棱两可:

// Notice "z" satisfy the condition of being a unique literal.
type A = { z: "A", x: string ; y: string }; 
type B = { z: "B", x: number; y: number };
type AorB = A | B;

function isAorB(p: AorB ) {
    if (p.z === "A") { 
        const onlyAx = p.x; // string
        const onlyAy = p.y; // string
        return;
    }
    const onlyBx = p.x; // number
    const onlyBy = p.y; // number
}

注意,使用以下形式的c现在调用isAorB(c)会导致错误。
const c = { x: "str", y: "str", z: "A" };

你可以这样做来确保 z

const c = { x: "str", y: "str", z: "A" as "A" };

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接