【问题标题】:How to assume two union types as the same如何假设两个联合类型相同
【发布时间】:2019-07-27 05:18:51
【问题描述】:

我正在制作一个具有两个相同联合类型作为参数的函数。 如何在 switch 语句中将它们假定为同一类型?

我正在尝试使用 Typescript@3.5.1

interface Square {
  kind: 'square'
  size: number
}
interface Rectangle {
  kind: 'rectangle'
  width: number
  height: number
}

type Shape = Square | Rectangle

function areas(s: Shape, ss: Shape) {
  if (s.kind !== ss.kind) return  // check if the kind of them are the same
  switch (s.kind) {
    case 'square':
      return s.size * s.size + ss.size * ss.size // error
    case 'rectangle':
      return s.height * s.width + ss.height * ss.width // error
  }
}

这个语句会出错,例如

“形状”类型上不存在属性“大小”。

类型“矩形”.ts(2339) 上不存在属性“大小”

但我预计不会发生错误,因为 s.kind 的等价性 并且ss.kind 已被选中。

【问题讨论】:

    标签: javascript typescript


    【解决方案1】:

    您收到错误是因为 sss 在整个过程中都只是 Shape。编译器知道两者都有一个名为“kind”的值,但仍然不知道每个的实际类型。 Square 可能有“大小”,但 Shape 没有,编译器知道。

    创建一个需要知道Shape 细节的函数首先违背了使用interface 的目的。你可以像这样更干净地实现你想要的:

    interface Square {
      area(): number
      size: number
    }
    
    interface Rectangle {
      area(): number
      width: number
      height: number
    }
    
    type Shape = Square | Rectangle
    
    function areas(s: Shape, ss: Shape) {
       return s.area() + ss.area()
    }
    

    但是,如果你真的想这样做,你可以通过在访问它的属性之前将每个对象显式转换为所需的类型来做到这一点

    interface Square {
      size: number
    }
    
    interface Rectangle {
      width: number
      height: number
    }
    
    type Shape = Square | Rectangle
    
    function areas(s: Shape, ss: Shape) {
        if (typeof s != typeof ss) {
            return
        }
        switch (typeof s) {
            case 'Square': {
                s = s as Square; ss = ss as Square
                return s.size * s.size + ss.size * ss.size
            }
            case 'Rectangle': {
                s = s as Rectangle; ss = ss as Rectangle
                return s.width * ss.height + s.width * ss.height
            }
        }
    }
    

    请注意,第二个示例实际上不会起作用(即使您将某些内容明确声明为联合类型之一),尽管自 typeof 编译后将返回 "object",但它演示了如何判断编译器使用哪种类型(使用as

    class SquareImpl implements Square {
        size: number = -1
    
        constructor(size : number) {
            this.size = size
        }
    }
    
    let s : Square = new SquareImpl(10)
    console.log(typeof s) // logs "object"
    

    您可以尝试使用instanceof 来实现它:

    if (s instanceof Square && ss instanceof Square) {
        s = s as Square; ss = ss as Square
        return s.size * s.size + ss.size * ss.size
    }
    // similar code for Rectangle etc
    

    但是,Typescript 不允许您使用检查对象是否在运行时实现接口,因此您又要使用自定义类型保护:

    interface Square {
        kind: string
        sameShape(obj: Shape): boolean 
        area(): number
        size: number
    } 
    
    class SquareImpl implements Square {
        kind: string = "square"
        size: number = -1
        area() { return this.size * this.size }
        sameShape(obj: Shape): obj is Square {
            return obj.kind == "square"
        }
        constructor(size: number) { this.size = size }
    }
    // similar for Rectangle
    
    ...
    let r : Rectangle = new RectangleImpl(1, 2)
    let s : Square = new SquareImpl(3)
    let ss : Square = new SquareImpl(2)
    if (s.sameShape(ss)) {
        console.log('s + ss: '+ s.area() + ss.area())
    }
    if (s.sameShape(r)) {
        console.log('s + r: '+ s.area() + r.area())
    }
    

    【讨论】:

    • 在这个特定问题中使用这个比instanceof 有优势吗?
    • @ErikPhilips instanceof 可以说是一个更好的选择,但这仍然不是最好的方法 - 只是试图复制 OP 的代码,同时仍然显示具有保存类型的属性名字不是必须的
    • 等等,为什么是s.constructor.name(可能只是"Object",因为没有人说有class Squareclass Rectangle)而不是s.kind?这是discriminated union 的一个近乎典型的例子;当您检查s.kind === 'square' 时,编译器当然明白sSquare 而不是Rectangle。 OP 问题中的问题与两种相关类型有关,此答案仅通过将 sss 断言为相同类型的类型来解决。
    • @jcalz 我同意。如果您想修改我的答案以解决这些问题,请继续。我想演示一个解决核心问题的解决方案,即“一旦我知道了这个对象是什么类型,我如何让编译器知道它是什么类型”,而不是“这个对象是什么类型”。无论哪种方式,我都不认为第二个版本完全可以解决 OP 提出的一般用例。
    【解决方案2】:

    让编译器相信联合类型的两个变量是correlated 在一般情况下并不容易,甚至是不可能的。编译器几乎总是认为两个这样的值是独立的,除非您分别测试它们。含义:这里唯一的解决方案看起来像:您只需做足够的工作来说服自己类型是相同的,并且您必须通过type assertion 之类的东西告诉编译器不要担心它:

    function areas(s: Shape, ss: Shape) {
      if (s.kind !== ss.kind) return; // check if the kind of them are the same
      switch (s.kind) {
        case "square":
          return s.size * s.size + (ss as typeof s).size * (ss as typeof s).size;
        case "rectangle":
          return (
            s.height * s.width + (ss as typeof s).height * (ss as typeof s).width
          );
      }
    }
    

    或者,您将不得不做比您认为必要的工作更多的工作,以便编译器确信唯一的可能性就是您所期望的。这意味着你会做你觉得像冗余类型警卫的事情。我认为在这种情况下,我会将您的代码重构为这样,这只增加了一项检查:

    function areas2(s: Shape, ss: Shape) {
      if (s.kind === "square" && ss.kind === "square") {
        return s.size * s.size + ss.size * ss.size;
      }
      if (s.kind === "rectangle" && ss.kind === "rectangle") {
        return (
          s.height * s.width + ss.height * ss.width
        );
      }
      return;
    }
    

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

    Link to code

    【讨论】:

      【解决方案3】:

      所以我看到了一些改进代码的方法。

      在我看来,Union Types 应该只在两种类型是不同类型时使用。例如,CSS 允许某些属性的值是字符串或数字。那么你如何向你的函数的使用者传达你只希望他们通过这两个中的一个呢?这是联合类型的一个很好的例子:

      var element: HtmlElement;Z
      function bad(width: any) {
        element.style.width = width;
      }
      // no typescript error about the wrong type being passed in
      bad(new Date());  
      
      type widthType = string | number | null;
      function good(width: widthType) {
        element.style.width = widthType
      }
      //  typescript error about the wrong type being passed in
      good(new Date());  
      

      Typescript Playground Example.

      虽然许多人决定使用kind 属性,但我尽可能避免使用它,因为它是Magic String。如果两种类型兼容,则必须有人知道您的广场的相互作用才能在那里建立自己的广场(yikes)。从技术上讲,您可以通过迁移到抽象类来避免这种情况:

      abstract class Square {
        static kind = 'square'
      }
      

      但是你可以只使用instanceOf,所以没有真正的意义。

      但是,在面向对象编程中,我们需要注意继承(is-a)和组合(has-a)。由于 Rectangle is-a 形状和 Square is-a 形状,那么我们应该这样建模我们的对象:

      interface Shape { }
      interface Rectangle : Shape { }
      interface Square : Shape { }
      

      现在我们对模型有了一个很好的关注点,我们需要看看这个方法。什么是区域?一个区域is the quantity that expresses the extend of a two dimensional figure or shape。所以我们现在应该修改我们的继承链/树/任何需要这个功能的东西:

      interface Shape { 
        areas(shape: Shape): number;
      }
      interface Rectangle : Shape { }
      interface Square : Shape { }
      

      我们encapsulate 在形状级别使用该方法,因为所有形状(假设为 2D 或更大)都有一个区域(0 仍然是一个大小)。

      很容易回顾这个和事情,为什么形状应该做这个计算,我只是建议许多框架(如果不是大多数 OOP 框架)做这个确切的事情。当您通过Equals 比较.Net 中的两个对象时,您应该始终测试类型是否相同。但请注意,该方法位于对象的根目录,而不是断开/全局方法。

      所以这可能是改进的一个好结果:

      interface Shape { 
        // null would indicate we can't join the two
        // I prefer null to indicate a known invalid value
        // and only use undefined to indicate an unknown (always invalid) value
        areas(shape: Shape): number | null;  
      }
      interface Rectangle : Shape { }
      interface Square : Shape { }
      
      class MyRectangle : Rectangle  {
        width: number;
        height: number;
        area(shape: Shape){
          if (!(shape instanceOf Rectangle)) {
            return null;
          }
          return this.height * this.width + shape.height * shape.width;
        }
      }
      
      class MySquare : Square {
        size: number;
        area(shape: Shape){
          if (!(shape instanceOf Square)) {
            return null;
          }
          return this.size * this.size + shape.size * shape.size;
        }
      }
      
      // Example call:
      
      const mySquare = new MySquare();
      const mySquare2 = new MySquare();
      const areas = mySquare2.area(mySquare);  // fully type checked.
      

      如果接口是一个单独的库而不是类,那么前面的例子很好,有人可能实际上想要以不同的方式表达这些值。如果不是这种情况,并且应该只有 1 种正方形和 1 种矩形,那么接口不是最佳选择,我建议改用类。因为在前面的示例中实现 Circle 将非常困难(接口和类都需要更改)。使用类看起来像:

      abstract class Shape {
        area(shape: Shape);
      }
      class Rectangle : Shape {
        width: number;
        height: number;
        area(shape: Shape){
          if (!(shape instanceOf Rectangle)) {
            return null;
          }
          return this.height * this.width + shape.height * shape.width;
        }
      }
      class Square: Shape {
        size: number;
        area(shape: Shape){
          if (!(shape instanceOf Square)) {
            return null;
          }
          return this.size * this.size + shape.size * shape.size;
        }
      }
      

      现在实现 Circle 变得微不足道了。

      class Circle: Shape {
        radius: number;
        area(shape: Shape){
          if (!(shape instanceOf Circle)) {
            return null;
          }
          return this.size * this.size + shape.size * shape.size;
        }
      }
      

      【讨论】:

        【解决方案4】:

        将您的 if 条件替换为 if (s.kind !=== ss.kind)

        希望对你有用

        【讨论】:

          猜你喜欢
          • 2020-06-05
          • 2022-08-12
          • 2022-12-18
          • 2018-02-23
          • 1970-01-01
          • 1970-01-01
          • 2011-05-17
          • 1970-01-01
          • 2019-06-22
          相关资源
          最近更新 更多