Consider a simple function draw
which can expect and enum for the type of the shape needs to draw as first argument and dimensions object as second argument.
const SHAPE_TYPES = {
CIRCLE: 'CIRCLE',
SQUARE: 'SQUARE',
TRIANGLE: 'TRIANGLE',
} as const;
type ShapeTypesEnum =
(typeof SHAPE_TYPES)[keyof typeof SHAPE_TYPES];
interface Circle {
radius: number;
}
interface Square {
sideLength: number;
}
interface Triangle {
base: number;
height: number;
}
type Dimensions = Circle | Square | Triangle;
function draw(shapeType: ShapeTypesEnum, dimensions: Dimensions): any {
// some logic
return dimensions;
}
Now, this allows us to pass wrong values to dimensions like,
const circle = draw(SHAPE_TYPES.CIRCLE, { sideLength: 5, });
So how can we make sure typescript will throw error if we pass wrong values in the above case?
Arguments Types Based on Enum
From the above example, ideally when we pass SHAPE_TYPES.CIRCLE
as type, the second argument dimensions should accept only radius
. For that we will use new interface instead of Union types.
// types from the above snippet
interface ShapeDimensions {
[SHAPE_TYPES.CIRCLE]: Circle,
[SHAPE_TYPES.SQUARE]: Square,
[SHAPE_TYPES.TRIANGLE]: Triangle,
}
function draw<T extends ShapeTypesEnum>(shapeType: T, dimensions: ShapeDimensions[T]): any {
// some logic
return dimensions;
}
const circle = draw(SHAPE_TYPES.CIRCLE, { radius: 5, });
Now, TypeScript will enforce that the dimensions argument matches the expected type based on the value of the shapeType argument.
Here is the TS playgound link.
Return Types Based on Enum
Now, let’s look into the return type. so far the above example we were using any
.
Consider we try to return the Union
type.
type Dimensions = Circle | Square | Triangle;
function draw(shapeType: ShapeTypesEnum, dimensions: Dimensions): Dimensions {
// some logic
return dimensions;
}
const circle = draw(SHAPE_TYPES.CIRCLE, { radius: 5, });
console.log('Return Circle :', circle.radius);
Now, typescript will give error in console.log
one way to remove this error is using guarding condition like below,
function isCircle(shape: Dimensions) : shape is Circle {
return "radius" in shape
}
if(isCircle(circle)) {
console.log('Return Circle :', circle.radius);
}
The better way to handle this is to return the right type based on the enum passed as first argument.
If we adopt, the approach as we used with arguments, we can avoid this guarding condition.
function draw<T extends ShapeTypesEnum>(shapeType: T, dimensions: ShapeDimensions[T]): ShapeDimensions[T] {
// some logic
return dimensions;
}
const circle = draw(SHAPE_TYPES.CIRCLE, { radius: 5, });
console.log('Return Circle :', circle.radius);
If we try to access circle.sideLength
, typescript will give the error.
But what if the return type is not a object literal and an instance of a class?
Consider the Circle
& Square
classes below
class Circle {
#radius = 0;
setRadius(radius: number) {
this.#radius = radius;
}
draw() {
// some logic
}
}
class Square {
#sideLength = 0;
setSideLength(sideLength: number) {
this.#sideLength = sideLength;
}
draw() {
// some logic
}
}
interface ShapeType {
[SHAPE_TYPES.CIRCLE]: Circle,
[SHAPE_TYPES.SQUARE]: Square,
}
For the ShapeFactory
if we try to return new Cirlce()
typescript will throw the error.
Either we need to explicitly typecast using as
class ShapeFactory {
createShape<T extends ShapeTypesEnum>(shape: T): ShapeType[T] {
if (shape === SHAPE_TYPES.CIRCLE) {
return new Circle() as ShapeType[T];
}
}
}
Another approach without typecasting will be as below,
class ShapeFactory {
createShape<T extends ShapeTypesEnum>(shape: T): ShapeType[T] {
return {
[SHAPE_TYPES.CIRCLE]: new Circle(),
[SHAPE_TYPES.SQUARE]: new Square(),
}[shape];
}
}
Feel free to check in TS playground
But using second approach means when ever we call the createFactory
we create instance of all types which is bit annoying.
Did I miss anything, or Is there a better way? Let me know you thoughts via email.
Thank You.
Versions of Language/packages used in this post.
Library/Language | Version |
---|---|
TypeScript | 5.5.3 |