TypeScript‘s type system is based on Structural typing. In a structurally typed system, a type is considered to be compatible with another type if the type has all the properties and methods of that type. But JavaScript is a duck-typed language. Since Typescript Compiles to JavaScript, you can take the benefit of duck typing also.
Table of Contents
Type Compatibility
“Type compatibility“ refers to the similarity of two types to each other. One of the essential 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 precision. We can easily substitute integer values everywhere a program it 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.
It is easier to compare primitive types like integers, strings, decimals, 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. The compatibility of the types is determined 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.
Structural typing is where two types are considered compatible if they have the same shape. The name of the type or how we create the object is immaterial
Duck typing is usage-based structural equivalence, which we usually find in dynamic languages like JavaScript which do not have a strong typing
Structural Typing in TypeScript
In this example, the interface Dog
& Cat
has the same property name
. But the Person
interface has the property firstName
& lastName
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | interface Dog { name:string } interface Cat { name:string } interface Person { firstName:string, lastName:string } |
We create two objects dog
& cat
using the Dog
& Cat
interface. Typescript does not complain when we assign dog
to cat
and vice versa. This is because both have the same shape.
1 2 3 4 5 6 7 8 | let dog:Dog = { name:"mars"} let cat:Cat = { name:"venus"} dog=cat; //ok cat=dog; //ok |
But we cannot assign person
object to dog
or to cat
. This is because the structure of person
is different from the structure of cat
or dog
1 2 3 4 5 6 | let person:Person ={firstName:"jon",lastName:"snow"} dog=person; // Property 'name' is missing in type 'Person' but required in type 'Dog' cat=person; //Property 'name' is missing in type 'Person' but required in type 'Cat'. |
In this example, we have added breedName
property to the Dog
interface.
1 2 3 4 5 6 7 8 9 10 | interface Dog { name:string breedName:string } interface Cat { name:string } |
Now we can assign dog
to cat
because a cat expects only a name
property, which dog
has. Additional properties of dog
does not make any difference.
But we cannot assign cat
to a dog
because dog
also expects a breedName
property and cat
does not have it.
1 2 3 4 5 6 7 | let dog:Dog = { name:"mars",breedName:"Bull dog"} let cat:Cat = { name:"venus"} cat=dog; dog=cat; //Property 'breedName' is missing in type 'Cat' but required in type 'Dog' |
You can make the breedName
property optional in the Dog
interface for the above code to work.
Classes and Structural Typing
This example shows the structural typing when using the classes. We can assign a cat to a dog and vice versa irrespective of the different classes from which we create them.
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 | class Dog { name:string; age:number constructor(name:string, age:number) { this.name=name; this.age=age } } class Cat { name:string; age:number constructor(name:string, age:number) { this.name=name; this.age=age } } let dog = new Dog("mars",20) let cat = new Cat("mars",20) dog=cat; //ok cat=dog; //ok |
But if we make age
property private in both classes then they become incompatible. The following code throws an error.
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 | class Dog { name:string; private age:number constructor(name:string, age:number) { this.name=name; this.age=age } } class Cat { name:string; private age:number constructor(name:string, age:number) { this.name=name; this.age=age } } let dog = new Dog("mars",20) let cat = new Cat("mars",20) dog=cat; //error cat=dog; //error //Type 'Cat' is not assignable to type 'Dog'. // Property 'age' is private in type 'Dog' but not in type 'Cat'. //Type 'Dog' is not assignable to type 'Cat'. // Property 'age' is private in type 'Dog' but not in type 'Cat' |
Duck Typing in TypeScript
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.
TypeScript does not allow Duck Typing. But JavaScript does.
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 is JavaScript code. We have two different objects person
and bankAccount
. Both are unrelated objects but have a one common function someFn
. The invokeSomeFn
invokes the someFn
of whatever object passed as an argument to it. We can pass any object to invokeSomeFn
and it will happily execute the someFn
from that object. The existence or nonexistence of any other property in the passed object is irrelevant to the invokeFn
. We call this behavior Duck typing. Duck typing does not check for compatibility.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //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"); } }; let invokeSomeFn = function (obj) { obj.someFn(); }; invokeSomeFn(person); invokeSomeFn(bankAccount |
TypeScript does not support duck typing. The following code is exactly the same as above but with types added. We annotate the argument of invokeSomeFn
as obj:Person
. Now invokeSomeFn
will not accept the BankAccount
as it has a different structure.
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 | interface Person { name:string, someFn: () => void } interface BankAccount { accountNo:number, someFn: () => void } let person:Person = { name:'Jon', someFn:function() { console.log("hello "+this.name + " king in the north") } } let bankAccount:BankAccount = { accountNo:100, someFn:function() { console.log("Please deppost some money") } } let invokeSomeFn= function(obj:Person) { obj.someFn() } invokeSomeFn(person) invokeSomeFn(bankAccount) //Argument of type 'BankAccount' is not assignable to parameter of type 'Person'. //Property 'name' is missing in type 'BankAccount' but required in type 'Person'. |
But we can get around it by creating a separate type consisting of someFn
only. In this example, we define an interface for the parameter inline. Now invokeSomeFn
will accept any object that has someFn
method in it. This trick, allow us to implement the Duck typing in TypeScript.
1 2 3 4 5 | let invokeSomeFn= function(obj:{someFn:() => void }) { obj.someFn() } |