如何假设两种联合类型相同

时间:2019-07-27 05:18:51

标签: javascript typescript

我正在创建一个具有两个相同的联合类型作为参数的函数。 在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已被选中。

4 个答案:

答案 0 :(得分: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())
}

答案 1 :(得分:0)

要说服编译器说联合类型的两个变量为correlated并不是一件容易的事情,甚至在一般情况下也是不可能的。除非您分别测试它们,否则编译器几乎总是会认为两个这样的值是独立的。含义:这里唯一的解决方案将是这样的:您仅做足够的工作来使自己确信类型相同,并且必须通过类似type assertion的方式告诉编译器不要担心:< / p>

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

答案 2 :(得分:0)

因此,我看到了一些改进代码的方法。

我认为Union Types仅应在两种类型是完全不同的类型时使用。例如,CSS允许某些属性的值可以是字符串或数字。那么,如何向功能使用者传达您只希望他们通过这两项中的一项?对于Union Type,这是一个很好的例子:

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,因此我尽量避免使用它。如果两种类型兼容,那么某人必须知道您的广场的相互作用,才能在这里建造自己的广场(类似)。从技术上讲,您可以通过转到抽象类来避免这种情况:

abstract class Square {
  static kind = 'square'
}

但是您只能使用instanceOf,所以没有实际意义。

但是,在面向对象编程中,我们需要了解继承(is-a)和组合(has-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很困难(接口和类都需要更改)。 Uses类看起来像:

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

答案 3 :(得分:0)

将if条件替换为 如果(s.kind!=== ss.kind)

希望它会为您服务