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.