
Table of contents
Open Table of contents
Problem statement
We have all either failed to understand or ignored this aspect of TypeScript’s type system: type compatibility.
So, what is type compatibility in TypeScript?
Type compatibility in TypeScript determines whether or not we can assign one type of value to another type of value.
Let’s consider an example:
let x: number = 30;
let y: number = 40;
x = y; // OK. ✅
y = x; // OK. ✅
You won’t be surprised to see that we can assign x to y
or y to x
. Both assignments work because they have the same TypeScript type, which is number
.
Now, let’s take another example:
let x: number = 30;
let y: string = "Joe";
x = y; // 💣 💥 Not OK, Error: Type 'string' is not assignable to type 'number'.
y = x; // 💣 💥 Not OK, Error: Type 'number' is not assignable to type 'string'.
In this example, it is clear that x
and y
cannot be assigned to each other due to their different types.
And this is precisely what type compatibility is about - it refers to how TypeScript determines whether we can or cannot assign one type of value to another.
Now, why is it crucial to bring attention to this concept?
Don’t we all already know this? Yes 💯 %
However, let’s delve into a few more examples where it tends to confuse some of us
Example 1
type Cat = {
name: string;
age: number;
};
type Dog = {
name: string;
age: number;
};
let myCat: Cat = {
name: "Luna",
age: 3,
};
let myDog: Dog = {
name: "Max",
age: 2,
};
myCat = myDog; // OK. ✅
myDog = myCat; // OK. ✅
Well, there are no errors in the code. myDog
is of type Dog
, and myCat
is of type Cat
. Despite having different types, we can effortlessly assign myDog to myCat
or vice versa.
Example 2
class Student {
name: string;
constructor(studentName: string) {
this.name = studentName;
}
}
class Teacher {
name: string;
constructor(teacherName: string) {
this.name = teacherName;
}
}
let mike: Student = new Teacher("James");
// OK. ✅
let james: Teacher = new Student("Mike");
// OK. ✅
You might wonder why this is work. The answer is that TypeScript’s types utilize a structural type system
.
If you are already familiar with the structural type system
, you can skip the rest of the article. However, if you’re not, let’s continue our journey and unravel the mystery.
Different Type system
Programming languages employ different type systems to handle data types.
(1) Nominal type system
(2) Structural type system
These two type systems, structural and nominal, offer different approaches to type checking in programming languages.
Now, let’s briefly explore them.
1. Nominal type system
In a nominal type system, the compatibility of types is determined based on their explicit names or declarations.
Meaning, two types with the same structure but different names are considered distinct and incompatible, even if their properties and methods are identical
💡 Programming languages such as
C#
,Java
,Swift
utilize a nominal type system.
lets take look on code snippet from Java
// #JAVA
class Teacher {
String name;
int age;
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
// (1)
Teacher teacherJames = new Teacher("James", 30);
// OK. ✅
// (2)
Teacher studentFrank = new Student("Frank", 34);
// Not OK. 💣 💥
// (3)
Student studentNina = new Student("Nina", 23);
// OK. ✅
// (4)
Student studentJohn = new Teacher("John", 26);
// Not OK. 💣 💥
(1) Here for variable teacherJames
, both the declaration type (Teacher
) and the instantiation type (Teacher
) match, this assignment is considered valid. ✅
(2) The variable studentFrank
is declared as type (Teacher
), but it is being assigned an object of type(Student
), Since the declared type and the instantiation type differ, this assignment is not considered valid in a nominal typing system. 💣 💥
(3) The variable studentNina
, The declaration and the instantiation types (Student
)match, so this assignment is valid. ✅
(4) The declared type (Student
)and the instantiation type (Teacher
) differ, this assignment is not considered valid in a nominal typing system. 💣 💥
Objects can be assigned to variables of the same or compatible types. However, assigning objects to variables with incompatible types, even if they have the same structure, is considered invalid in nominal typing.
2. Structural type system
In a structural type system, the compatibility of types is determined by their structure or shape rather than by their explicit names or declarations.
Meaning, if two objects have the same properties and methods, they are considered to be of the same type, regardless of their explicit type names.
If we take same java code snippet from above and write it in typescript it will all work due to structural typing.
class Teacher {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Student {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// (1)
let teacherJames: Teacher = new Teacher("James", 30);
// OK. ✅
// (2)
let studentFrank: Teacher = new Student("Frank", 34);
// OK. ✅
// (3)
let studentNina: Student = new Student("Nina", 23);
// OK. ✅
// (4)
let studentJohn: Student = new Teacher("John", 26);
// OK. ✅
💡 Programming languages such as OCaml, Haskell, Elm, Go, and Rust utilize a structural type system, and Typescript is also among them.
Let’s take one more example to further illustrate the concept.
interface Cat {
name: string;
age: number;
}
interface Dog {
name: string;
age: number;
}
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
let person = new Person("James", 30);
// object: { name: 'James', age: string }
// has type stucture { name: string, age: number }
let myCat: Cat = {
name: "Luna",
age: 3,
};
// has type stucture { name: string, age: number }
let myDog: Dog = {
name: "Max",
age: 2,
};
// has type stucture { name: string, age: number }
myCat = myDog; // OK. ✅
myDog = myCat; // OK. ✅
myCat = person; // OK. ✅
myDog = person; // OK. ✅
person = myCat; // OK. ✅
Here, the types Dog
, Cat
, and the object or instance created from the Person
class all have the same attributes: name (string)
and age (number)
. In TypeScript, type compatibility is determined by structural typing, which means that the above assignment will be successful.
💡 Rule: The type names aren’t important in TypeScript type compatibility - it is the structure that matters.
Challenges with Type Structure Inconsistencies
So far, we have observed that we can assign one type of value to another as long as both types have similar attributes with matching types. However, what happens when the attributes are unequal between the types?
Let’s explore an example:
class Student {
name: string;
constructor(studentName: string) {
this.name = studentName;
}
}
class Teacher {
name: string;
age: number;
constructor(teacherName: string, age: number) {
this.name = teacherName;
this.age = age;
}
}
let mike: Student = new Teacher("James", 34); //✅ OK
let james: Teacher = new Student("Mike"); // 💣 💥 Not OK, Error: Property 'age' is missing in type 'Student' but required in type 'Teacher'.
In this case, we have a Student
class and a Teacher
class. While we can successfully assign a Teacher
object to a Student
variable, the reverse assignment is not allowed. Here Student
class type fail to satisfy complete type structure of Teacher
One more example with illustration
type Cat = {
name: string;
age: number;
};
type Dog = {
name: string;
age: number;
breed: string;
};
let myCat: Cat = {
name: "Luna",
age: 3,
};
let myDog: Dog = {
name: "Max",
age: 2,
breed: "Labrador",
};
myCat = myDog; // ✅ OK
myDog = myCat; // 💣 💥 Not Ok, Error: Property 'breed' is missing in type 'Cat' but required in type 'Dog'.
In this example, the type Dog
has an additional attribute called breed
. Surprisingly, we can still assign myDog
to myCat
without any errors.
However, the reverse assignment is not allowed. We cannot assign myCat
to myDog
because myCat
lacks the breed
attribute and fails to satisfy the complete type structure of Dog
, which is {name: string, age: number, breed: string}
.
💡 Rule: An object,
a
, can be assigned to another object,b
(b = a), ifa
contains all the members thatb
expects, the assignment can be made successfully.
Closing Notes
- Understanding type compatibility is crucial in TypeScript as it determines whether one type of value can be assigned to another.
- TypeScript employs a structural type system, where compatibility is based on the structure or shape of types, rather than their explicit names or declarations.
- The structural type system allows for more flexible and intuitive type assignments based on shared attributes and methods.
Keep exploring the world of TypeScript and enjoy the benefits of its type system!
<> with ❤️
Next
Continuing from here, we’ll explore the following topics in next article:
- Type compatibility of
Function
Classes
Generics
- Understanding the distinctions between Variance, Covariance, Contravariance, and Bivariance in TypeScript.