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.
Table of Contents
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //C# Example using System; public class Program { public static void Main() { decimal decVar1 = 100.15m; decimal decVar2 = 150.15m; int intVar1 = 100; int intVar2 = 150; addDec(decVar1,decVar2); //250.30 addDec(intVar1,intVar2); //250 addInt(intVar1,intVar2); //250 //Error //cannot convert from 'decimal' to 'int' //addInt(decVar1,decVar2); } static void addDec(decimal agr1, decimal arg2) { Console.WriteLine(agr1 + arg2); } static void addInt(int agr1, int arg2) { Console.WriteLine(agr1 + arg2); } } |
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.
- Nominal typing uses the name to compare types
- 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
- type name must match or
- 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | using System; public class Program { public static void Main() { Dog dog = new Dog("Mars"); Cat cat = new Cat("Venus"); makeNoise(dog); //makeNoise(cat); //error CS1503: Argument 1: cannot convert from 'HelloWorld.Cat' to 'HelloWorld.Dog' } static public void makeNoise(Dog obj) { obj.makeNoise(); } //Dog & Cat are similar public class Dog { public String name; public Dog(string name) { this.name=name; } public void makeNoise() { Console.WriteLine ("Woof"); } } public class Cat { public String name; public Cat(string name) { this.name=name; } public void makeNoise() { Console.WriteLine ("Miau"); } } } |
We create two objects, a dog and a cat, from the types Dog and Cat Respectfully.
1 2 3 4 | Dog dog = new Dog("Mars"); Cat cat = new Cat("Venus"); |
The MakeNoise function takes a Dog and invokes makeNoise method
1 2 3 4 5 6 | static public void makeNoise(Dog obj) { obj.makeNoise(); } |
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.
1 2 3 4 5 6 | makeNoise(dog); //makeNoise(cat); //error CS1503: Argument 1: cannot convert from 'HelloWorld.Cat' to 'HelloWorld.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.
1 2 3 4 5 6 7 8 | public class BullDog:Dog { public String breedName; public BullDog(string name, string breedName) : base(name) { this.breedName=breedName; } } |
Now, you can use the bulldog
as a replacement for Dog
type. The makeNoise
method will accept bulldog
object.
1 2 3 4 | BullDog bulldog = new BullDog("Mars","BullDog"); makeNoise(bulldog); |
Now, update makeNoise
method to accept BullDog
instead of Dog
.
1 2 3 4 5 | static public void makeNoise(BullDog obj) { obj.makeNoise(); } |
Now, we cannot pass Dog
to makeNoise
. We cannot substitute types with their super type or parent type.
1 2 3 4 5 6 | Dog dog = new Dog("Mars"); makeNoise(dog); //error CS1503: Argument 1: cannot convert from 'Dog' to 'BullDog' |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Dog { constructor(public name: string) { } makeNoise() { console.log('Woof') } } class Cat { constructor(public name: string) { } makeNoise() { console.log('Miau') } } let dog = new Dog("Mars"); let cat = new Cat("Venus"); function makeNoise(obj: Dog) { obj.makeNoise(); } makeNoise(dog) //Ok makeNoise(cat) //Ok |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Person { constructor(public firstName: string, public lastName:string) { } makeNoise() { console.log('Helloooo') } } let person= new Person("Jon","Snow") makeNoise(person) //Error //Argument of type 'Person' is not assignable to parameter of type 'Dog'. // Property 'name' is missing in type 'Person' but required in type 'Dog'. |
The code works even if the Person
class has additional property. As long as the Person has name
and makeNoise
property it works.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Person { constructor(public name:string,public address: string) { } makeNoise() { console.log('Helloooo') } } let person= new Person("Jon Snow","North") makeNoise(person) //Works ok |
You can pass any random object as long it has name
and makeNoise
method
1 2 3 4 5 6 7 8 9 10 11 12 | let obj = { name:'test', address:'addresss', employeer:'', makeNoise() { console.log('Testing') } } makeNoise(obj) |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //Javascript let person = { name:'Jon', someFn:function() { console.log("hello "+this.name + " king in the north") } } let bankAccount = { accountNo:'100', someFn:function() { console.log("Please deppost some money") } } invokeSomeFn= function(obj) { obj.someFn() } invokeSomeFn(person) invokeSomeFn(bankAccount) |
If the object does not have the makeNoise
JavaScript simply throws the TypeError.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Person { constructor(name) { this.name = name; } } let person = new Person("John"); function makeNoise(obj) { obj.makeNoise(); } makeNoise(person); //TypeError: obj.makeNoise() is not a function |
1 2 3 4 5 6 7 8 | sayHello(person) //Hello John sayHello(employee) //Hello John function sayHello(obj) { console.log ("Hello " + obj.name); } |
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.