Type Compatibility

Type Compatibility

Type Compatibility (as we discuss here) determines if one thing can be assigned to another. E.g. string and number are not compatible:

Soundness

TypeScript's type system is designed to be convenient and allows for unsound behaviours e.g. anything can be assigned to any which means telling the compiler to allow you to do whatever you want:

Structural

TypeScript objects are structurally typed. This means the names don't matter as long as the structures match

This allows you to create objects on the fly (like you do in vanilla JS) and still have safety whenever it can be inferred.

Also more data is considered fine:

Variance

Variance is an easy to understand and important concept for type compatibility analysis.

For simple types Base and Child, if Child is a child of Base, then instances of Child can be assigned to a variable of type Base.

This is polymorphism 101

In type compatibility of complex types composed of such Base and Child types depends on where the Base and Child in similar scenarios is driven by variance.

  • Covariant : (co aka joint) only in same direction

  • Contravariant : (contra aka negative) only in opposite direction

  • Bivariant : (bi aka both) both co and contra.

  • Invariant : if the types aren't exactly the same then they are incompatible.

Note: For a completely sound type system in the presence of mutable data like JavaScript, invariant is the only valid option. But as mentioned convenience forces us to make unsound choices.

Functions

There are a few subtle things to consider when comparing two functions.

Return Type

covariant: The return type must contain at least enough data.

Number of arguments

Fewer arguments are okay (i.e. functions can choose to ignore additional parameters). After all you are guaranteed to be called with at least enough arguments.

Optional and Rest Parameters

Optional (pre determined count) and Rest parameters (any count of arguments) are compatible, again for convenience.

Note: optional (in our example bar) and non optional (in our example foo) are only compatible if strictNullChecks is false.

Types of arguments

bivariant : This is designed to support common event handling scenarios

Also makes Array<Child> assignable to Array<Base> (covariance) as the functions are compatible. Array covariance requires all Array<Child> functions to be assignable to Array<Base> e.g. push(t:Child) is assignable to push(t:Base) which is made possible by function argument bivariance.

This can be confusing for people coming from other languages who would expect the following to error but will not in TypeScript:

Enums

  • Enums are compatible with numbers, and numbers are compatible with enums.

  • Enum values from different enum types are considered incompatible. This makes enums useable nominally (as opposed to structurally)

Classes

  • Only instance members and methods are compared. constructors and statics play no part.

  • private and protected members must originate from the same class. Such members essentially make the class nominal.

Generics

Since TypeScript has a structural type system, type parameters only affect compatibility when used by a member. For example, in the following T has no impact on compatibility:

However, if T is used, it will play a role in compatibility based on its instantiation as shown below:

In cases where generic arguments haven't been instantiated they are substituted by any before checking compatibility:

Generics involving classes are matched by relevant class compatibility as mentioned before. e.g.

FootNote: Invariance

We said invariance is the only sound option. Here is an example where both contra and co variance are shown to be unsafe for arrays.

Last updated

Was this helpful?