Skip to content

Understanding Type Compatibility in TypeScript

Posted on:May 31, 2023 at 01:25 PM

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), if a contains all the members that b expects, the assignment can be made successfully.

Closing Notes

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:

References:

Discuss on