Computer scienceProgramming languagesTypeScriptData TypesThinking of types as sets of values

Type compatibility

9 minutes read

Type compatibility in TypeScript refers to the ability of one type to be assigned to another type. It determines whether a value of one type can be used in a context that expects a different type. In this topic, we will explore how TypeScript determines if one item can be assigned to another; essentially, how it determines whether types are compatible.

Basics of type compatibility

TypeScript uses a structural type system, which means that types are based on their structure or shape rather than their name or declaration. If two types have compatible structures, they are considered compatible, even if they are defined separately.

Let's look at an example:

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

interface Dog {
  name: string;
  age: number;
  breed: string;
}

let animal: Animal = { name: "Max", age: 5 };
let dog: Dog = { name: "Buddy", age: 3, breed: "Labrador" };

animal = dog; // Valid assignment due to type compatibility
dog = animal; // Error: Property 'breed' is missing in type 'Animal'

In this example, the Animal and Dog interfaces have the same structure, with name and age properties. Therefore, an object of type Dog can be assigned to a variable of type Animal because the required properties match. However, assigning an Animal object to a Dog variable results in an error because the Dog type has an additional breed property.

If a target type has optional properties that are not present in the source type, the assignment is still considered valid.

interface Person {
  name: string;
  age?: number; // Optional property
}

let person: Person = { name: "John" };
let anotherPerson: Person = { name: "Jane", age: 25 };

person = anotherPerson; // Valid assignment due to optional properties

In this example, the Person interface has an optional age property. When assigning an object with or without the age property to a variable of type Person, TypeScript considers it a valid assignment because the optional property does not need to be present.

Type compatibility with built-ins

In TypeScript, basic type compatibility refers to the compatibility between the built-in types and their respective subtypes. Here are some examples of basic type compatibility:

// The number type in TypeScript is compatible with both number and NaN.
let num: number = 42;
let nan: NaN = NaN;

num = nan; // Valid assignment
nan = num; // Valid assignment

// The string type in TypeScript is compatible with string and string literals.
let str: string = "Hello";
let literal: "World" = "World";

str = literal; // Valid assignment
literal = str; // Invalid assignment - Type 'string' is not assignable to type '"World"'

// The boolean type in TypeScript is compatible with boolean and true/false literals.
let bool: boolean = true;
let literal: true = true;

bool = literal; // Valid assignment
literal = bool; // Valid assignment

// The undefined and null types in TypeScript are compatible with their respective subtypes.
let undef: undefined = undefined;
let nullable: null = null;

undef = nullable; // Valid assignment
nullable = undef; // Valid assignment

Comparing functions

TypeScript also checks the compatibility of function types. It considers the parameter types and the return type of the function when determining compatibility. Functions with fewer parameters are considered compatible with functions that expect more parameters. However, the types of the parameters and the return type must be compatible.

Parameter compatibility: For functions to be compatible, the parameters must have compatible types. TypeScript enforces that each parameter in the assigned function type should either have the same type or be assignable to the corresponding parameter in the target function type.

type Adder = (a: number, b: number) => number;
type Multiplier = (x: number, y: number) => number;

let add: Adder = (a, b) => a + b;
let multiply: Multiplier = (x, y) => x * y;

// Assigning multiply function to add variable
add = multiply; // Valid assignment

In this example, the Adder and Multiplier types represent different function types. The add function expects two parameters of type number, and the multiply function also expects two parameters of the same type. Since the parameter types are compatible, the assignment of multiply to add is valid.

Return type compatibility: The return type of a function must be compatible or a subtype of the expected return type. TypeScript ensures that the assigned function's return type matches the expected return type or is assignable to it.

type Mapper = (value: number) => string;
type Transformer = (value: number) => boolean;

let map: Mapper = (value) => value.toString();
let transform: Transformer = (value) => value > 0;

// Assigning map function to transform variable
transform = map; // Error: Return types are not compatible

In this example, the Mapper and Transformer types represent different function types. The map function of type Mapper returns a string, while the transform function of type Transformer expects a boolean return type. Since the return types are not compatible, the assignment of map to transform results in an error.

Here's another example regarding the number of parameters:

type Adder = (a: number, b: number) => number;
type Multiplier = (a: number, b: number, c: number) => number;

let add: Adder = (a, b) => a + b;
let multiply: Multiplier = (a, b, c) => a * b * c;

add = multiply; // Valid assignment due to fewer parameters
multiply = add; // Error: Type '(a: number, b: number) => number' is not assignable to type '(a: number, b: number, c: number) => number'

In this example, the Adder type represents a function that takes two parameters and returns a number, while the Multiplier type represents a function that takes three parameters and returns a number. TypeScript allows assigning the multiply function to the add variable because the multiply function has more parameters than required by the Adder type. However, assigning the add function to the multiply variable results in an error because the add function has fewer parameters than required by the Multiplier type.

TypeScript provides flexibility when comparing functions by allowing interchangeability between optional and required parameters and treating rest parameters as infinite optional parameters. This allows for more versatile function assignments and usage.

Type compatibility also applies to enums, classes, and generics in TypeScript. Let's explore each of them.

With enums, classes and generics

Enums: Enums in TypeScript are compatible with numbers and vice versa. Additionally, enums with the same numeric values are considered compatible, even if they belong to different enum types.

enum Color {
  Red,
  Green,
  Blue,
}

let colorValue: Color = Color.Red;
let numericValue: number = 0;

numericValue = colorValue; // Valid assignment
colorValue = numericValue; // Valid assignment

In this example, the colorValue of type Color can be assigned to a variable of type number, and vice versa, because enums are compatible with numbers.

Classes: TypeScript checks the compatibility of the instance side of classes, which includes the instance members such as properties and methods. The static side of classes, which includes static members and the constructor, is not considered for type compatibility.

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }
}

let animal: Animal = new Animal("Max");
let dog: Dog = new Dog("Buddy", "Labrador");

animal = dog; // Valid assignment
dog = animal; // Error: Property 'breed' is missing in type 'Animal'

In this example, the Animal class and the Dog class have a similar structure, but the Dog class has an additional breed property. Therefore, an instance of the Dog class can be assigned to a variable of type Animal, but not vice versa.

Private members affect type compatibility in TypeScript, as they are taken into account during type checking. TypeScript ensures that private members are not accessible outside their declaring class, which can impact the compatibility between types containing private members.

Generics: TypeScript's type compatibility extends to generics as well. When comparing generic types, TypeScript checks if the generic type parameters are compatible.

interface Box<T> {
  value: T;
}

let box1: Box<number> = { value: 42 };
let box2: Box<string> = { value: "Hello" };

box1 = box2; // Error: Type 'Box<string>' is not assignable to type 'Box<number>'
box2 = box1; // Error: Type 'Box<number>' is not assignable to type 'Box<string>'

In this example, even though both box1 and box2 have the same structure, the generic type parameters differ (number vs. string). TypeScript considers them incompatible and the assignments result in errors.

Type compatibility in enums, classes, and generics helps ensure that values are used correctly and maintains type safety in TypeScript.

Conclusion

Type compatibility in TypeScript helps ensure that values are used correctly and reduces the likelihood of runtime errors. It allows for more flexibility in working with different types and enables better type inference and type checking in the language. In this topic, we looked at the basics of type compatibility and how it's determined with different data types.

How did you like the theory?
Report a typo