# Polymorphism vs Switch Statements with examples in TypeScript

## What is Polymorphism?

Polymorphism is a concept in object-oriented programming that allows objects of different classes to be treated as if they are objects of a common base class or interface. This means that you can write code that operates on objects of a base type, and then substitute those objects with objects of derived types that implement the same base type. The code that operates on the objects of the base type will automatically work with the derived objects as well because they all implement the same interface.

## Why it's better than Switch Statements

Using polymorphism instead of `switch` statements can make your code more flexible and maintainable.

### Example with Animal

Let's say we have an interface called `Animal` which defines a method `makeSound`:

```typescript
interface Animal {
  makeSound(): void;
}
```

We also have a `Zoo` class which contains an array of `Animal` objects, and a method `letAnimalsMakeSound` that loops over each animal and calls its `makeSound` method:

```typescript
class Zoo {
  animals: Animal[] = [];

  letAnimalsMakeSound() {
    for (const animal of this.animals) {
      animal.makeSound();
    }
  }
}
```

Now let's say we have two classes that implement the `Animal` interface: `Dog` and `Cat`. The `makeSound` method of the `Dog` class should output "Woof!", while the `makeSound` method of the `Cat` class should output "Meow!".

With a `switch` statement, we might implement the `makeSound` method of the `Dog` and `Cat` classes like this:

```typescript
class Dog implements Animal {
  makeSound() {
    console.log("Woof!");
  }
}

class Cat implements Animal {
  makeSound() {
    console.log("Meow!");
  }
}
```

And we might use a `switch` statement in the `letAnimalsMakeSound` method of the `Zoo` class to call the `makeSound` method of each animal:

```typescript
class Zoo {
  animals: Animal[] = [];

  letAnimalsMakeSound() {
    for (const animal of this.animals) {
      switch(animal.constructor.name) {
        case "Dog":
          (animal as Dog).makeSound();
          break;
        case "Cat":
          (animal as Cat).makeSound();
          break;
      }
    }
  }
}
```

This implementation works, but it has a few drawbacks:

* It's not very flexible. If we add a new type of animal, we'll need to update the `switch` statement in the `Zoo` class.
    
* It's not very maintainable. The `Zoo` class needs to know about all the types of animals in order to call their `makeSound` methods.
    
* It's not very testable. We can't easily test each type of animal separately, because the `Zoo` class has tight coupling with all the types of animals.
    

On the other hand, with polymorphism, we can implement the `Dog` and `Cat` classes like this:

```typescript
class Dog implements Animal {
  makeSound() {
    console.log("Woof!");
  }
}

class Cat implements Animal {
  makeSound() {
    console.log("Meow!");
  }
}
```

And we can simply call the `makeSound` method of each animal in the `letAnimalsMakeSound` method of the `Zoo` class, without needing to know the type of each animal:

```typescript
class Zoo {
  animals: Animal[] = [];

  letAnimalsMakeSound() {
    for (const animal of this.animals) {
      animal.makeSound();
    }
  }
}
```

This implementation has several benefits:

* It's more flexible. We can easily add new types of animals without needing to update the `Zoo` class.
    
* It's more maintainable. The `Zoo` class doesn't need to know about all the types of animals in order to call their `makeSound` methods.
    
* It's more testable. We can test each type of animal separately, because the `Zoo` class doesn't have tight coupling with all the types of animals.
    

Additionally, using polymorphism can help make our code more modular and extensible. For example, let's say we want to add a new feature to our `Animal` interface: the ability to move. We can add a new method `move` to the `Animal` interface and we can implement it in the `Dog` and `Cat` classes like this:

```typescript
interface Animal {
  makeSound(): void;
  move(): void;
}

class Dog implements Animal {
  makeSound() {
    console.log("Woof!");
  }

  move() {
    console.log("Running!");
  }
}

class Cat implements Animal {
  makeSound() {
    console.log("Meow!");
  }

  move() {
    console.log("Jumping!");
  }
}
```

Now, we can call the `move` method of each animal in the `letAnimalsMove` method of the `Zoo` class:

```typescript
class Zoo {
  animals: Animal[] = [];

  letAnimalsMakeSound() {
    for (const animal of this.animals) {
      animal.makeSound();
    }
  }

  letAnimalsMove() {
    for (const animal of this.animals) {
      animal.move();
    }
  }
}
```

### Example with Shapes

Consider the following example code that uses `switch` statements to handle different types of shapes:

```typescript
interface Shape {
  type: string;
  area(): number;
}

class Circle implements Shape {
  type = 'circle';
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle implements Shape {
  type = 'rectangle';
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

function calculateArea(shape: Shape) {
  let area: number;

  switch(shape.type) {
    case 'circle':
      area = (shape as Circle).area();
      break;
    case 'rectangle':
      area = (shape as Rectangle).area();
      break;
    default:
      area = 0;
      break;
  }

  return area;
}

const myCircle = new Circle(5);
console.log(calculateArea(myCircle)); // Output: 78.53981633974483

const myRectangle = new Rectangle(10, 5);
console.log(calculateArea(myRectangle)); // Output: 50
```

In this code, the `calculateArea` function switches on the `type` property of the `Shape` interface to determine which specific method to call. However, if we add more shapes to our code in the future, we will need to modify this function to handle them.

Instead of using a `switch` statement, we can use polymorphism to handle different types of shapes without modifying our code. We can add a `calculateArea` method to the `Shape` interface, and then implement it in each of the derived classes:

```typescript
interface Shape {
  type: string;
  calculateArea(): number;
}

class Circle implements Shape {
  type = 'circle';
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle implements Shape {
  type = 'rectangle';
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  calculateArea() {
    return this.width * this.height;
  }
}

function printArea(shape: Shape) {
  console.log(shape.calculateArea());
}

const myCircle = new Circle(5);
printArea(myCircle); // Output: 78.53981633974483

const myRectangle = new Rectangle(10, 5);
printArea(myRectangle); // Output: 50
```

In this code, the `printArea` function takes a `Shape` object and calls its `calculateArea` method, without switching on its `type` property. This code will work with any object that implements the `Shape` interface, even if we add new shapes in the future.

In addition to making your code more flexible and maintainable, using polymorphism instead of `switch` statements can also make your code more testable. When you use polymorphism, you can write tests for each derived class separately, without needing to write tests that cover all possible combinations of types.

Consider the following example test code for the `calculateArea` function that uses a `switch` statement:

```typescript
describe('calculateArea', () => {
  it('calculates the area of a circle', () => {
    const myCircle = new Circle(5);
    expect(calculateArea(myCircle)).toBe(78.53981633974483);
  });

  it('calculates the area of a rectangle', () => {
    const myRectangle = new Rectangle(10, 5);
    expect(calculateArea(myRectangle)).toBe(50);
  });

  it('returns 0 for an unknown shape type', () => {
    const myShape = { type: 'unknown' };
    expect(calculateArea(myShape)).toBe(0);
  });
});
```

In this code, we need to test all possible combinations of shape types that the `calculateArea` function can handle, including an unknown shape type. This can become difficult to maintain as we add more shapes to our code.

Instead of using a `switch` statement, we can use polymorphism to make our code more testable. We can create separate test cases for each derived class, and test the `calculateArea` method of each class separately:

```typescript
describe('calculateArea', () => {
  it('calculates the area of a circle', () => {
    const myCircle = new Circle(5);
    expect(myCircle.calculateArea()).toBe(78.53981633974483);
  });

  it('calculates the area of a rectangle', () => {
    const myRectangle = new Rectangle(10, 5);
    expect(myRectangle.calculateArea()).toBe(50);
  });
});
```

In this code, we only need to test the `calculateArea` method for each derived class. This makes our tests more focused and easier to maintain.

Using polymorphism allows us to add new features to our code without needing to update all the classes that implement the `Animal` interface. We can simply add the new method to the interface, and each implementing class can choose to implement it or not.

In summary, using polymorphism instead of `switch` statements can help make our code more flexible, maintainable, and testable, and can also help make our code more modular and extensible.

%%[chatgpt]
