Type Compatibility (as we discuss here) determines if one thing can be assigned to another. E.g. string and number are not compatible:
let str:string="Hello";let num:number=123;str = num; // ERROR: `number` is not assignable to `string`num = str; // ERROR: `string` is not assignable to `number`
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:
let foo:any=123;foo ="Hello";// Laterfoo.toPrecision(3); // Allowed as you typed it as `any`
Structural
TypeScript objects are structurally typed. This means the names don't matter as long as the structures match
interfacePoint { x:number, y:number}classPoint2D {constructor(public x:number,public y:number){}}let p:Point;// OK, because of structural typingp =newPoint2D(1,2);
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:
interfacePoint2D { x:number; y:number;}interfacePoint3D { x:number; y:number; z:number;}var point2D:Point2D= { x:0, y:10 }var point3D:Point3D= { x:0, y:10, z:20 }functioniTakePoint2D(point:Point2D) { /* do something */ }iTakePoint2D(point2D); // exact match okayiTakePoint2D(point3D); // extra information okayiTakePoint2D({ x:0 }); // Error: missing information `y`
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.
/** Type Hierarchy */interfacePoint2D { x:number; y:number; }interfacePoint3D { x:number; y:number; z:number; }/** Two sample functions */letiMakePoint2D= ():Point2D=> ({ x:0, y:0 });letiMakePoint3D= ():Point3D=> ({ x:0, y:0, z:0 });/** Assignment */iMakePoint2D = iMakePoint3D; // OkayiMakePoint3D = iMakePoint2D; // ERROR: Point2D is not assignable to Point3D
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.
let iTakeSomethingAndPassItAnErr= (x: (err:Error, data:any) =>void) => { /* do something */ };iTakeSomethingAndPassItAnErr(() =>null) // OkayiTakeSomethingAndPassItAnErr((err) =>null) // OkayiTakeSomethingAndPassItAnErr((err, data) =>null) // Okay// ERROR: Argument of type '(err: any, data: any, more: any) => null' is not assignable to parameter of type '(err: Error, data: any) => void'.iTakeSomethingAndPassItAnErr((err, data, more) =>null);
Optional and Rest Parameters
Optional (pre determined count) and Rest parameters (any count of arguments) are compatible, again for convenience.
letfoo= (x:number, y:number) => { /* do something */ }letbar= (x?:number, y?:number) => { /* do something */ }letbas= (...args:number[]) => { /* do something */ }foo = bar = bas;bas = bar = foo;
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
/** Event Hierarchy */interfaceEvent { timestamp:number; }interfaceMouseEventextendsEvent { x:number; y:number }interfaceKeyEventextendsEvent { keyCode:number }/** Sample event listener */enumEventType { Mouse, Keyboard }functionaddEventListener(eventType:EventType,handler: (n:Event) =>void) {/* ... */}// Unsound, but useful and common. Works as function argument comparison is bivariantaddEventListener(EventType.Mouse, (e:MouseEvent) =>console.log(e.x +","+e.y));// Undesirable alternatives in presence of soundnessaddEventListener(EventType.Mouse, (e:Event) =>console.log((<MouseEvent>e).x +","+ (<MouseEvent>e).y));addEventListener(EventType.Mouse, <(e:Event) =>void>((e:MouseEvent) =>console.log(e.x +","+e.y)));// Still disallowed (clear error). Type safety enforced for wholly incompatible typesaddEventListener(EventType.Mouse, (e:number) =>console.log(e));
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:
/** Type Hierarchy */interfacePoint2D { x:number; y:number; }interfacePoint3D { x:number; y:number; z:number; }/** Two sample functions */letiTakePoint2D= (point:Point2D) => { /* do something */ }letiTakePoint3D= (point:Point3D) => { /* do something */ }iTakePoint3D = iTakePoint2D; // Okay : ReasonableiTakePoint2D = iTakePoint3D; // Okay : WHAT
Enums
Enums are compatible with numbers, and numbers are compatible with enums.
enumStatus { Ready, Waiting };let status =Status.Ready;let num =0;status = num; // OKAYnum = status; // OKAY
Enum values from different enum types are considered incompatible. This makes enums useable nominally (as opposed to structurally)
enumStatus { Ready, Waiting };enumColor { Red, Blue, Green };let status =Status.Ready;let color =Color.Red;status = color; // ERROR
Classes
Only instance members and methods are compared. constructors and statics play no part.
classAnimal { feet:number;constructor(name:string, numFeet:number) { /** do something */ }}classSize { feet:number;constructor(meters:number) { /** do something */ }}let a:Animal;let s:Size;a = s; // OKs = a; // OK
private and protected members must originate from the same class. Such members essentially make the class nominal.
/** A class hierarchy */classAnimal { protected feet:number; }classCatextendsAnimal { }let animal:Animal;let cat:Cat;animal = cat; // OKAYcat = animal; // OKAY/** Looks just like Animal */classSize { protected feet:number; }let size:Size;animal = size; // ERRORsize = animal; // ERROR
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:
interfaceEmpty<T> {}let x:Empty<number>;let y:Empty<string>;x = y; // okay, y matches structure of x
However, if T is used, it will play a role in compatibility based on its instantiation as shown below:
interfaceNotEmpty<T> { data:T;}let x:NotEmpty<number>;let y:NotEmpty<string>;x = y; // error, x and y are not compatible
In cases where generic arguments haven't been instantiated they are substituted by any before checking compatibility: