Duck typing in Typescript

Let us discuss everyone’s favourite topic since Typescript was invented - ‘duck typing’.

Duck typing is type safety checks for complex types. It gets its name following the adage -

If it walks like a duck and quacks like a duck, it must be a duck.

Typescript modifies it slightly to -

Don’t check whether it is a duck. If it quacks like a duck.. etc., it must be a duck

Consider an example with a couple of variables of type objects and derived from a class.

class Fruit {
  shape = "round";
}

class Plant {
  shape = "line";
}

const apple: Fruit = new Fruit();
const orange: Fruit = new Fruit();

In the past we have seen that objects are not set to the same rules as everyone else when it comes to specific operations. We cannot directly compare objects to get a valid result - they will be executed by reference rather than doing one-to-one comparison of object entries.

console.log(apple == orange); // false

Any type compatibility checks across objects (derived from same or different classes) will get complicated. But at the same time, we may not really worry about a class to derive the object from as long as the class has all of the properties of the object type.

Now, also mix this background of a problem with an additional complexity. Typescript strives to be compatible with non-typed objects within Typescript and variables from Javascript.

How would you ensure that a new object can interact in a type-safe way with a class and another object under these circumstances?

Enter “duck typing”.

With duck typing, Typescript compares signatures of classes and allows object of one type to be used with an instance of another if the object’s type signature is same as, or is a subset of, the initiating class’s signature.

Let’s see examples!

The following initializations are valid since both Fruit and Flower have the same variable - shape.

class Fruit {
  shape = "round";
}

class Flower {
  shape = "beautiful";
}

const apple: Fruit = new Fruit();
const daffodil: Fruit = new Flower();
const rose: Flower = new Fruit();

Let’s add another class Plant with an additional variable.

class Fruit {
  shape = "round";
}

class Flower {
  shape = "beautiful";
}

class Plant {
  shape = "straight line";
  allWeather = "false";
}

const orange: Fruit = new Plant();

We see that all props of the type Fruit are available when we initialize Plant. So, this becomes a perfectly valid initialization.

However, we cannot do the opposite.

const egg: Plant = new Fruit();
// error TS2741: Property 'allWeather' is missing in type 'Fruit' but required in type 'Plant'.

Plant type has a variable called allWeather, which is not available with the initializing class Fruit. This causes a compile error.

Moral of the story

Typescript does not care about the initializing class as long as it has all the props and methods of the class used for the type. So, keep calm and carry on typing.

comments powered by Disqus