Issue
I would like to make a mapper to easily get the function to draw a shape.
For now, I have a problem inside the mapper and I don't see how to fix it.
enum ShapeType {
  POLYGON = 'polygon',
  RECTANGLE = 'rectangle'
}
interface Polygon {
  type: ShapeType.POLYGON;
  points: Array<[number, number]>;
}
interface Rectangle {
  type: ShapeType.RECTANGLE;
  points: [[number, number], [number, number]];
}
type Shape = Polygon | Rectangle;
const mapper: Record<ShapeType, (shape: Shape) => void> = {
  // Argument of type 'Shape' is not assignable to parameter of type 'Polygon'
  [ShapeType.POLYGON]: (shape) => drawPolygon(shape),
  // Argument of type 'Shape' is not assignable to parameter of type 'Rectangle'
  [ShapeType.RECTANGLE]: (shape) => drawRectangle(shape)
};
const drawPolygon = (toDraw: Polygon): void => {}
const drawRectangle = (toDraw: Rectangle): void => {}
const toDraw: Array<Shape> = [];
toDraw.forEach((shape) => mapper[shape.type](shape));
I also need to support the behavior inside toDraw.forEach where mapper[shape.type](shape) is accepted.
Solution
Your mapper type isn't accurate. Record<ShapeType, (shape: Shape) => void> means that each method must accept an arbitrary Shape.  With that type, it should be completely valid to call mapper[ShapeType.POLYGON](rectangle) or mapper[ShapeType.RECTANGLE](polygon), so the compiler is warning you that you can't assume that shape is a Polygon or that shape is a Rectangle.  You could try to fix that by adding some runtime check, like
const mapper: Record<ShapeType, (shape: Shape) => void> = {
  [ShapeType.POLYGON]: (shape) => {
    if (shape.type !== ShapeType.POLYGON) throw new Error("NO!");
    drawPolygon(shape)
  },
  [ShapeType.RECTANGLE]: (shape) => {
    if (shape.type !== ShapeType.RECTANGLE) throw new Error("NO!");
    drawRectangle(shape)
  }
};
but now there's not much point in having a mapper at all; you might as well just have a single shape handler like
const handler = (shape: Shape) => {
  switch (shape.type) {
    case ShapeType.POLYGON: return drawPolygon(shape);
    case ShapeType.RECTANGLE: return drawRectangle(shape);
  }
}
And then your toDraw.forEach() simply becomes:
const toDraw: Array<Shape> = [];
toDraw.forEach((shape) => handler(shape));
If, for some reason, you really want to dynamically look up handlers in a mapper, then you will need to significantly refactor your types and operations, as described in microsoft/TypeScript#47109. It involves writing a "basic" key-value interface and then expressing everything as generic indexes into that interface, or into mapped types over that interface.  That allows the compiler to "see" the abstract relationship between mapper[shape.type] and shape when shape is generic.
Here's the basic mapping interface
interface ShapeLookup {
  [ShapeType.POLYGON]: {
    points: Array<[number, number]>;
  };
  [ShapeType.RECTANGLE]: {
    points: [[number, number], [number, number]];
  }
}
Now you can write Shape as a generic "distributive object type" (as coined in ms/TS#47109):
type Shape<K extends ShapeType = ShapeType> =
  { [P in K]: { type: P } & ShapeLookup[P] }[K]
So Shape<ShapeType.POLYGON> is equivalent to your Polygon, and Shape<ShapeType.RECTANGLE> is equivalent to your Rectangle.  Indeed, assuming you still want them, you can recover your original Polygon and Rectangle interfaces from those:
interface Polygon extends Shape<ShapeType.POLYGON> { }
interface Rectangle extends Shape<ShapeType.RECTANGLE> { }
And Shape with no generic type argument is equivalent to Shape<ShapeType>, which is the union equivalent to Polygon | Rectangle, just like before.
Now we can write the type of mapper as a mapped type over ShapeType:
const mapper: { [K in ShapeType]: (shape: Shape<K>) => void } = {
  [ShapeType.POLYGON]: (shape) => drawPolygon(shape), // okay!
  [ShapeType.RECTANGLE]: (shape) => drawRectangle(shape) // okay!
};
That works because the compiler sees that the method for ShapeType.POLYGON accepts a Shape<ShapeType.POLYGON> which is a Polygon, which is what drawPolygon accepts.  And similarly for ShapeType.RECTANGLE.
Now, whenever you want to write mapper[shape.type](shape), you'll want shape to be generic, of a type like Shape<K>.  This works well inside of forEach() because you can make the callback a generic function like this:
const toDraw: Array<Shape> = [];
toDraw.forEach(<K extends ShapeType>(
  shape: Shape<K>) => mapper[shape.type](shape) // okay!
);
                            Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.