Structural, Nominal, and Duck typing

Structural, Nominal, or Duck typing are the different methods by which Type Systems compares the compatibility and equivalence of data types. In this tutorial, let us dig into this and find the differences and similarities between them.

Type Compatibility

“Type compatibility” refers to the similarity of two types to each other. One of the important tasks of a type system is to determine if the two given types are compatible with each other or if a type is a subtype of another type.

Type Compatibility is important because if a type is compatible with another type, then you can convert it and substitute it in operations involving another type.

Consider the primitive data types integer and decimal. We can convert an integer value to a decimal value without losing any precision. We can easily substitute integer values everywhere a program expects a decimal type. Hence, we can say that the integer is compatible with the decimal data type.

Type T1 compatibility with T2 does not mean that T2 is also compatible with T1. An integer is compatible with a decimal, but a decimal type is definitely not compatible with an integer. Converting a decimal value into an integer definitely results in a loss of precision. Hence, they are not compatible.

This C# example demonstrates the above. The code contains two functions addDec & addInt. We can pass an integer to a addDec function, but the compiler throws an error if we try to pass decimal to addInt method

It is easier to compare the primitive types like integer, string, decimal, etc. They have a simple structure. But objects, classes, etc., have complex structures. There are two ways in which type systems compare the types to each other for compatibility. One is nominal typing and the other one is structural typing.

  1. Nominal typing uses the name to compare types
  2. Structural typing uses the structure to compare types.

Duck typing is usage-based structural equivalence, which we usually find in dynamic languages which do not have a strong typing

Nominal Typing

The nominal systems determine compatibility by explicit declarations and/or the names of their types. Each type is unique in the nominal system. Even if they have the same data and shape, we cannot assign them across types.

For the types to be compatible in a nominal system

  1. type name must match or
  2. we explicitly declare the type as a subtype of another type. Here the type is compatible with its parent type.

The best examples of nominal type systems are C# & Java.

Take the following Dog and Cat class is written in C#. Note that both the objects have name property and makeNoise method.

We create two objects, a dog and a cat, from the types Dog and Cat Respectfully.

The MakeNoise function takes a Dog and invokes makeNoise method

It will work with a dog object, but not with a cat object. The C# compiler throws the error. cannot covert from Cat to Dog. Although the shape of both objects is the same, C# treats them as incompatible with each other. Hence, we cannot substitute a cat for a dog.

This is how the nominal type systems work. The nominal system always treats the objects are incompatible if it created from different types (unless they have inheritance relationship).

Nominal Typing with Subtypes

We can also create a type as a subtype of another type. This is also known as inheritance. The inherited type is always compatible with its Parent. But Parent is not compatible with their child

Let us create a subtype BullDog by extending the Dog type.

Now, you can use the bulldog as a replacement for Dog type. The makeNoise method will accept bulldog object.

Now, update makeNoise method to accept BullDog instead of Dog.

Now, we cannot pass Dog to makeNoise. We cannot substitute types with their super type or parent type.

Structural Typing

In structural typing, a type is considered compatible with a supertype if it has all the members of the supertype and, optionally, additional members. Here, the shape of the type is more important than its name.

TypeScript uses structural typing to check for equivalence.

The following is the Typescript equivalent of the C# code from the previous section.

The example has two types Dog and Cat. Both these types have the same structure. We create a dog and cat instance and invoke makeNoise. Although the makeNoise expects a dog instance, it does not complain when we pass a cat instance. This is because Typescript will accept any object that has a shape that is the same as that of the Dog class. How we create the instance is immaterial in the structural typing type system.

In this example, the Person class does have a makeNoise method. But it does not have name property. Hence its shape is not the same as that of the Dog class. Invoking makeNoise with Person results in a compiler error.

The code works even if the Person class has additional property. As long as the Person has name and makeNoise property it works.

You can pass any random object as long it has name and makeNoise method

Duck Typing

Duck typing neither cares about the name nor the structure of the type. It must have the given method or properties required by the operation. We find duck typing only in dynamically typed languages like JavaScript.

The duck type originates from the phrase “If it walks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck“. It means that if an entity behaves like a duck then you can safely assume it is probably a duck. In the type system, if you are expecting a certain behavior and the object has those behaviors then you can use that object. Its shape and type are not important.

This JavaScript example contains two objects person and a bankAccount. They neither share the same type nor have the same structure. But they have one common method someFn. The invokeSomeFn functions accept both the objects without any issue. In fact, you can pass anything to invokeSomeFn as long as that type has someFn method.

If the object does not have the makeNoise JavaScript simply throws the TypeError.

Nominal vs Structural vs Duck

Duck typing is the most flexible while nominal typing is the least flexible. But highly error-prone and bugs are difficult to track.

In the Nominal typing system, we need to specify the type of the data explicitly or implicitly. This is more code, but easily readable code and offers less flexibility. The Nominal systems are less error-prone and bugs are easy to find and fix.

In structural typing, we do not need to specify the type. it offers more flexibility than a nominal typing system but does not stop you from passing the wrong object to an operation. Such bugs are difficult to track.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top