In my code, I have implemented a factory function that generates shapes based on a discriminated union of shape arguments. Here is an example:
interface CircleArgs { type: "circle", radius: number };
interface SquareArgs { type: "square", length: number };
type ShapeArgs = CircleArgs | SquareArgs;
class Circle { constructor(_: CircleArgs) {}}
class Square { constructor(_: SquareArgs) {}}
type Shape = Circle | Square;
type ShapeOfArgs<Args extends ShapeArgs> =
Args extends CircleArgs? Circle :
Square;
function createShape<Args extends ShapeArgs>(args: Args): ShapeOfArgs<Args> {
switch (args.type) {
case "circle": return new Circle(args);
case "square": return new Square(args);
}
}
This approach helps TypeScript infer the correct return type from the argument as demonstrated below:
const circle1 = createShape({type: "circle", radius: 1}); // inferred as Circle
const square1 = createShape({type: "square", length: 1}); // inferred as Square
Even when wrapping the call in another function, the return type is still propagated correctly:
class Container { insert(_: Shape): void {}; }
function createShapeIn<Args extends ShapeArgs>(args: Args, cont: Container) {
const shape = createShape(args);
cont.insert(shape);
return shape;
}
const cont = new Container();
const circle2 = createShapeIn({type: "circle", radius: 1}, cont); // still inferred as Circle
const square2 = createShapeIn({type: "square", length: 1}, cont); // still inferred as Square
However...
Introducing one more type called Figure
breaks the implementation.
Let's add a new type of shapes named Figure
:
interface FigureArgs { type: "figure", amount: number };
class Figure { constructor(_: FigureArgs) {}}
The updated union types become:
type ShapeArgs = CircleArgs | SquareArgs | FigureArgs;
type Shape = Circle | Square | Figure;
As a result, the conditional type needs to be modified to accommodate the changes:
type ShapeOfArgs<Args extends ShapeArgs> =
Args extends CircleArgs? Circle :
Args extends SquareArgs? Square :
Figure;
Unfortunately, after making these adjustments, the transpilation fails with errors for each shape type assignment in the switch statement.
function createShape<Args extends ShapeArgs>(args: Args): ShapeOfArgs<Args> {
switch (args.type) {
case "circle": return new Circle(args); // error: Circle not assignable ShapeOfArgs<Args>
case "square": return new Square(args); // error: Square not assignable ShapeOfArgs<Args>
case "figure": return new Figure(args); // error: Figure not assignable ShapeOfArgs<Args>
}
}
What am I overlooking in this setup?
PS: Though using the as
operator may offer a workaround, it undermines the purpose of type checking.