【问题标题】:How to get concrete type from mapping variable?如何从映射变量中获取具体类型?
【发布时间】:2021-08-28 17:29:20
【问题描述】:

我有以下代码:

const enum ShapeType {
  Circle,
  Rectangle
}

class Shape {
  constructor(public shapeType: ShapeType) {}
}

class Circle extends Shape {
  constructor(public x: number, public y: number, public r: number) {
    super(ShapeType.Circle);
  }
}

class Rectangle extends Shape {
  constructor(public x: number, public y: number, public w: number, public h: number) {
    super(ShapeType.Rectangle);
  }
}

function handleRectangleRectangleCollision(r1: Rectangle, r2: Rectangle) {
  return Helpers.doRectanglesCollide(r1.x, r1.y, r1.w, r1.h, r2.x, r2.y, r2.w, r2.h)
}

function handleRectangleCircleCollision(r: Rectangle, c: Circle) {
  return Helpers.circleRectangleCollision(c.x, c.y, c.r, r.x, r.y, r.w, r.h);
}

function handleCircleCircleCollision(c1: Circle, c2: Circle) {
  return Helpers.circlesCollide(c1.x, c1.y, c1.r, c2.x, c2.y, c2.y);
}

function handleCircleRectangleCollision(c: Circle, r: Rectangle) {
  return Helpers.circleRectangleCollision(c.x, c.y, c.r, r.x, r.y, r.w, r.h);
}

export let colliderMapping = {
  [ShapeType.Rectangle]: {
    [ShapeType.Rectangle]: handleRectangleRectangleCollision,
    [ShapeType.Circle]: handleRectangleCircleCollision
  },
  [ShapeType.Circle]: {
    [ShapeType.Circle]: handleCircleCircleCollision,
    [ShapeType.Rectangle]: handleCircleRectangleCollision
  }
}

function doShapesCollide(s1: Shape, s2: Shape) {
  let colliderFn = colliderMapping[s1.shapeType][s2.shapeType];

  return colliderFn(s1, s2);
}

最后一个错误:

return colliderFn(s1, s2);

Argument of type 'Shape' is not assignable to parameter of type 'Rectangle & Circle'.
  Type 'Shape' is missing the following properties from type 'Rectangle': x, y, w, h

我明白为什么我会收到错误(我认为),但我不知道如何解决它。我基本上是在尝试通过使用映射变量来实现一种干净的双重调度方式,这样每个形状组合都会返回一个有效的函数,我可以调用该函数来查看它们是否发生冲突。

有没有办法做到这一点?如果有,怎么做?

【问题讨论】:

    标签: typescript types collision-detection double-dispatch


    【解决方案1】:

    请看看我的article

    考虑这个超级简单的例子:

    type A = {
      check: (a: string) => string
    }
    
    type B = {
      check: (a: number) => number
    }
    
    type C = {
      check: (a: symbol) => number
    }
    
    type Props = A | B | C;
    declare var props:Props;
    
    props.check() // (a: never) => string | number
    

    为什么 check 需要 never 而不是所有可能类型的联合?

    因为函数参数处于逆变位置,所以它们被合并到never中,因为string & number & symbol是never;

    尝试将 check 参数的类型更改为某个对象:

    type A = {
        check: (a: { a: 1 }) => string
    }
    
    type B = {
        check: (a: { b: 1 }) => number
    }
    
    type C = {
        check: (a: { c: 1 }) => number
    }
    
    type Props = A | B | C;
    declare var props: Props;
    
    //(a: { a: 1;} & { b: 1;} & { c: 1;}) => string | number
    props.check()
    

    很明显,你有所有可能的参数类型的交集。

    有几种解决方法。

    可以添加条件语句:

    function doShapesCollide(s1: Shape, s2: Shape) {
        if (s1.shapeType === ShapeType.Circle && s2.shapeType === ShapeType.Circle) {
            let colliderFn = colliderMapping[s1.shapeType][s2.shapeType];
            return colliderFn(s1, s2); // should be ok
        }
    }
    

    上述方法仍然会导致编译错误,因为s1ShapecolliderFn 期望CircleCircleShape 的子类型,并且更具体 - 因此它不起作用。

    为了让它工作,你应该添加另一个条件:

    
    function doShapesCollide(s1: Shape | Circle, s2: Shape | Circle) {
        if (s1.shapeType === ShapeType.Circle && s2.shapeType === ShapeType.Circle) {
            let colliderFn = colliderMapping[s1.shapeType][s2.shapeType];
            if (s1 instanceof Circle && s2 instanceof Circle) {
                return colliderFn(s1, s2); // should be ok
            }
        }
    
    }
    

    它有效,但它很丑。不是吗?

    您还可以创建多个类型保护,使代码更简洁,但添加更多业务逻辑。

    或者您可以将函数的并集转换为交集,换句话说,您可以产生函数重载。

    const enum ShapeType {
        Circle,
        Rectangle
    }
    
    class Shape {
        constructor(public shapeType: ShapeType) { }
    }
    
    class Circle extends Shape {
        constructor(public x: number, public y: number, public r: number) {
            super(ShapeType.Circle);
        }
    }
    
    class Rectangle extends Shape {
        constructor(public x: number, public y: number, public w: number, public h: number) {
            super(ShapeType.Rectangle);
        }
    }
    
    function handleRectangleRectangleCollision(r1: Rectangle, r2: Rectangle) {
    }
    
    function handleRectangleCircleCollision(r: Rectangle, c: Circle) {
    }
    
    function handleCircleCircleCollision(c1: Circle, c2: Circle) {
    }
    
    function handleCircleRectangleCollision(c: Circle, r: Rectangle) {
    }
    
    export let colliderMapping = {
        [ShapeType.Rectangle]: {
            [ShapeType.Rectangle]: handleRectangleRectangleCollision,
            [ShapeType.Circle]: handleRectangleCircleCollision
        },
        [ShapeType.Circle]: {
            [ShapeType.Circle]: handleCircleCircleCollision,
            [ShapeType.Rectangle]: handleCircleRectangleCollision
        }
    }
    
    // credits goes to https://stackoverflow.com/a/50375286
    type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
        k: infer I
    ) => void
        ? I
        : never;
    
    function doShapesCollide(s1: Shape, s2: Shape) {
        let colliderFn = colliderMapping[s1.shapeType][s2.shapeType];
        type Overload =
            & UnionToIntersection<typeof colliderFn>
            & ((r1: Rectangle | Circle | Shape, r2: Rectangle | Circle | Shape) => void)
        const overloaded = colliderFn as Overload
        return overloaded(s1, s2); // should be ok
    }
    
    

    Playground

    以上更改不需要您更改业务逻辑。

    【讨论】:

      【解决方案2】:

      我注意到handleRectangleCircleCollisionhandleCircleRectangleCollision 做了同样的事情,但只是改变了传递参数的顺序。

      function handleRectangleCircleCollision(r: Rectangle, c: Circle) {
        return Helpers.circleRectangleCollision(c.x, c.y, c.r, r.x, r.y, r.w, r.h);
      }
      
      function handleCircleRectangleCollision(c: Circle, r: Rectangle) {
        return Helpers.circleRectangleCollision(c.x, c.y, c.r, r.x, r.y, r.w, r.h);
      }
      

      您还使用类和继承(OOP 方法)以及类之外的函数。

      我试图减少代码并保持简单。 这是一个使用类型保护的解决方案。我还用交集类型替换了类。

      type Point = { x: number; y: number; }
      type Rectangle = { w: number; h: number; } & Point
      type Circle = { r: number } & Point
      type Shape = Rectangle | Circle
      
      function isCircle(shape: Shape): shape is Circle {
        return 'r' in shape;
      }  
      function handleShapeCollision(shapeA: Shape, shapeB: Shape) {
        if (isCircle(shapeA)) 
        return handleCircleCollision(shapeA, shapeB);
        return handleRectangleCollision(shapeA, shapeB);
      }
      function handleCircleCollision(circle: Circle, shape: Shape) {
        if (isCircle(shape)) 
        return Helpers.circlesCollide(circle.x, circle.y, circle.r, shape.x, shape.y, shape.y);
        return Helpers.circleRectangleCollision(circle.x, circle.y, circle.r, shape.x, shape.y, shape.w, shape.h);
      }
      function handleRectangleCollision(r: Rectangle, shape: Shape) {
        if (isCircle(shape)) 
        return Helpers.circleRectangleCollision(shape.x, shape.y, shape.r, r.x, r.y, r.w, r.h);
        return Helpers.doRectanglesCollide(r.x, r.y, r.w, r.h, shape.x, shape.y, shape.w, shape.h)
      }
      

      这里是TypescriptPlayground。随意玩耍。

      【讨论】:

        猜你喜欢
        • 2018-11-16
        • 2015-08-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多