TypeScript - Type Compatibility



In TypeScript, type compatibility refers to the ability to assign one type of variable, object, etc. to another type. In other words, it refers to the ability to check whether two types are compatible with each other based on the structure of them.

For example, string and boolean types are not compatible with each other as shown in the code.

let s:string = "Hello";
let b:boolean = true;
s = b; // Error: Type 'boolean' is not assignable to type 'string'
b = s; // Error: Type 'string' is not assignable to type 'boolean'

TypeScript's type system allows to perform certain operations that are not safe at compile time. For example, any types of variable are compatible with 'any', which is unsound behavior.

For example,

let s: any = 123;
s = "Hello"; // Valid

How TypeScript Performs Type Compatibility Checks?

Typescript uses the structural subtyping and structural assignment for performing the type compatibility checks. Let's learn each with examples.

Structural Subtyping

TypeScript uses the structural subtyping method to check whether the particular type is the subtype of another type. Even if the name of the members of a particular type doesn't match but the structure matches, the TypeScript compiler considers both types the same.

For example,

interface Person {
    name: string;
}

let person: Person;
let obj = { name: "John", age: 30 };

// Ok
person = obj;

To check whether the type of the 'obj' object is compatible with the type of the Person interface, typescript checks if 'obj' contains at least all properties and methods included in the Person interface.

TypeScript doesn't care about the extra members added to the subtype. Here, the obj object contains an extra 'age' property but still, it is compatible with the Person type as the obj object contains the 'name' property of the string type.

If the 'obj' object doesn't contain all members of the Person interface, it is not assignable to the object with Person type. For example,

interface Person {
    name: string;
}

let person: Person;
let obj = { n: "John", age: 30 };

// Not Ok
person = obj;

The above code throws an error when we compile it as a type of 'obj' object that is not the same as the Person interface.

How to Use Type Compatibility Effectively?

Developers can use the interface and generics to use the types effectively in TypeScript. Here are the best tips for using the interface and generics for type compatibility.

Using Interfaces

Using an interface developer can define contracts or types to ensure that implementation adheres to these types. It is useful in ensuring the type compatibility across different parts of your code.

Let's understand it via the example below.

Example

In the example below, the 'user2' variable has the type 'User'. So, developers can assign objects having the same properties as the 'User' interface to the 'user2' object.

interface User {
    name: string;
    age: number;
}

const user = { name: "Alice", age: 30 };
let user2: User = user;
console.log(user2)

On compiling, it will generate the following JavaScript code.

const user = { name: "Alice", age: 30 };
let user2 = user;
console.log(user2);

Output

Its output is as follows –

{ name: 'Alice', age: 30 }

Using Generics

We can use generics to create reusable components that can work with multiple data types instead of single data types. It allows developers to pass the type as a parameter and use that type for variables, objects, classes, function parameters, etc.

Let's understand it via the example below.

Example

In the code below, we have a 'Wrapper' interface that takes the data type as a parameter. We have created stringWrapper and numberWrapper variables and passed string and number data types as an argument.

// Define a generic interface with a single property
interface Wrapper<T> {
    value: T;
}

// Use the interface with a string type
let stringWrapper: Wrapper<string> = {
    value: "Hello, TypeScript!",
};

// Use the interface with a number type
let numberWrapper: Wrapper<number> = {
    value: 123,
};

console.log(stringWrapper.value); // Output: Hello, TypeScript!
console.log(numberWrapper.value); // Output: 123

On compiling, it will generate the following JavaScript code.

// Use the interface with a string type
let stringWrapper = {
    value: "Hello, TypeScript!",
};

// Use the interface with a number type
let numberWrapper = {
    value: 123,
};
console.log(stringWrapper.value); // Output: Hello, TypeScript!
console.log(numberWrapper.value); // Output: 123

Output

The above example code will produce the following output –

Hello, TypeScript!
123

Functions and Type Compatibility

When we compare or assign one function to another function, the TypeScript compiler checks whether the target function has at least the same arguments and returns the type as the source function. If you are assigning function 'x' to function 'y', function 'x' is a target function, and function 'y' is a source function. Additional parameters in function 'y' won't cause any errors.

Example

In the code below, function 'x' contains only 1 parameter and function 'y' contains 2 parameters. When we assign function 'x' to function 'y', additional parameter 's' won't cause any error.

// Defining functions 
let x = (a: number) => { console.log(a); };
let y = (b: number, s: string) => { console.log(b + s); };

y = x; // OK
// x = y; // Error: x does not accept two arguments.

Classes and Type Compatibility

When we compare two classes, it compares members of instances only. The class constructor and static members belong to the class itself, so those are not included in the comparison.

Example

In this code, we have the same instance members in variable 'a' and variable 'b'. So, when we assign variable 'a' to 'b', it won't raise any error.

class Animal {
  feet: number;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet: number;
  constructor(meters: number) {}
}

let a: Animal;
let s: Size;
// Works because both classes have the same shape (they have the same instance properties).
a = s;

You can use the interface and generics for type compatibility. When it comes to the function, the target function should have at least the same parameters as the source function. For the type compatibility of classes, they should have the same instance members.

Advertisements