TypeScript - return and arguments type based on enum

17 Jul 2024

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.

Show TS error for args

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

Show TS error for return type

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);

Showing no error on return type

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
If you find my work helpful, You can buy me a coffee.