🔷 TypeScript Q39 / 101

What is type compatibility?

AI-Powered Answer ✓ Answered

Type compatibility in TypeScript is a fundamental concept that determines whether one type can be assigned to, or used in place of, another type. Unlike many other object-oriented languages that use nominal typing, TypeScript employs a structural subtyping system, often referred to as 'duck typing'.

What is Structural Subtyping (Duck Typing)?

In TypeScript, two types are compatible if they have similar 'shapes' or structures, regardless of where they originated. If type A has at least all the members of type B, then A is compatible with B (i.e., A can be assigned to B). This is often summarized as: 'If it walks like a duck and quacks like a duck, then it's a duck.'

This approach differs from nominal typing, where compatibility is determined by explicit declarations or the name of the type. For instance, in a nominally typed language, a variable of type 'Animal' could only hold objects explicitly declared as 'Animal' or a subtype of 'Animal'. In TypeScript, if an object has the required properties of 'Animal', it can be treated as an 'Animal'.

How Type Compatibility Works

Object Compatibility

When comparing two object types, the 'source' type (the type being assigned) must have at least all the members that the 'target' type (the type it's being assigned to) declares. Excess properties in the source type are generally ignored, but literal objects might trigger 'excess property checks' for immediate assignment.

typescript
interface Named {
  name: string;
}

let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y; // OK, y has a 'name' property

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

let p: Person;
let anotherPerson = { name: 'Bob', age: 30 };
p = anotherPerson; // OK

let incompletePerson = { name: 'Charlie' };
// p = incompletePerson; // Error: Property 'age' is missing in type '{ name: string; }'

Function Compatibility

For functions, compatibility is determined by their parameters and return types. The rules are a bit more nuanced:

  • Parameter List (Bivariance): A function with fewer parameters is compatible with a function that has more parameters, as long as the types of the common parameters match. TypeScript allows 'bivariance' for parameters by default, meaning parameters can be 'wider' or 'narrower' than the target type. This can sometimes lead to unsafe behavior but is usually desired for callback patterns. (Note: strictFunctionTypes compiler option restricts this to contravariance for function parameters).
  • Return Type: The return type of the source function must be assignable to the return type of the target function (covariance).
typescript
let func1 = (a: number, b: number) => 0;
let func2 = (a: number) => 0;

func2 = func1; // OK: func2 can accept fewer arguments than func1
// func1 = func2; // Error (if 'strictFunctionTypes' is on, otherwise OK due to bivariance)

interface EventListener {
  (e: Event): void;
}

interface MouseEventListener {
  (e: MouseEvent): void; // MouseEvent is a subtype of Event
}

let listener: EventListener;
let mouseListener: MouseEventListener = (e: MouseEvent) => console.log(e.clientX);

listener = mouseListener; // OK: MouseEvent is assignable to Event
mouseListener = listener; // OK by default (bivariance), potentially unsafe if listener expects more general Event

Special Considerations

  • Classes: Classes work similarly to object literals. If two classes have the same shape, they are compatible. However, private and protected members must originate from the same declaration to be compatible, making classes with such members nominally typed with respect to those members.
  • Enums: Enums are compatible with numbers, and members of different enum types are not compatible with each other unless their underlying values are numbers and they match.
  • Generics: Generic types are structurally compatible based on the instantiated types. If a generic type argument isn't used in any structural way, its compatibility is ignored.
  • any and unknown: any is compatible with all types and all types are compatible with any. unknown is less permissive; all types are assignable to unknown, but unknown is only assignable to any or itself.
  • readonly properties: readonly properties only need to be present in the target type, not necessarily marked readonly in the source type for assignment compatibility (though they must be assignable). When comparing two readonly properties, they must also be compatible.

Understanding type compatibility is crucial for writing robust and flexible TypeScript code, as it dictates how different parts of your system can interact without explicit casting or type assertions.