Subtyping is when an object of one type can be used when an object of another type is expected. Example 1: Animal and Birds interface Animal { food : String walkingSpeed : number } interface Bird { food : String walkingSpeed : number flightSpeed: number } An Animal can eat and walk, and we can represent the details of the animal with an interface type that keeps track of the animal's food and walking speed. A Bird is a kind of animal, so we should also be able to track its food and walking speed. We say that Bird is a subtype of Animal (subtype of basically means kind of). We write this as Bird <: Animal Record type A is a subtype of record type B if A has all the fields that B does, with the same types. For example, a Bird has all the fields that an Animal does, plus some additional ones. This is expressed in the subtyping rule for record types. Subtyping is important because we can treat a subtype like a supertype. If we have the code: let b:Bird = { food: 'bread', walkingSpeed: 5, flightSpeed: 20 } let a:Animal = b print('This animal walks at ' + a.walkingSpeed) it will run fine, because the bird has all the fields we expect an animal to have. The opposite won't work: if we write let a:Animal = { food: 'bread', walkingSpeed: 5} let b:Bird = a // type error: Animal is not a subtype of Bird print('This bird flies at ' + b.flightSpeed) we'll get a type error on the second line, since Animal is not a subtype of Bird. Formally, we say that a Bird is substitutable for an Animal, because we can safely substitute a Bird whenever we expect an Animal. What is the subtyping rule for functions? Let's think about the return type first, using this example: let getBird : () -> Bird = () => { food: 'bread', walkingSpeed: 5, flightSpeed: 20 } get getAnimal : () -> Animal = getBird getAnimal().walkingSpeed This code will work just fine. A function that returns a Bird clearly also returns an animal. So if we have a function of type X -> B and another of type X -> A, where A<:B, then X -> A is a subtype of X -> B. We say that functions are covariant in their return position: here co- means "subtypes in the same direction as" and the subtyping relationship between functions is the same as the subtyping relationship between their return types. What about arguments? This is more tricky. You might expect the same thing to work. But what about this example: let a:Animal = { food: 'bread', walkingSpeed: 5} let doFlight: Bird -> String = (b:Bird) => "this bird flies at " + b.flightSpeed let doSomething: Animal -> String = doFlight // type error: Bird-> String is not a subtype of Animal -> String doSomething(a) The code will break if we run it, because the function doFlight only works correctly if it is passed a bird. But if we assign doFlight to doSomething and call doSomething with an animal, then the code in doFlight won't be able to get the flightSpeed from the animal. Thus the assignment from doFlight to doSomething is illegal. In fact the rule for subtyping says that if B<:A, then A -> X <: B -> X. This is a contravariant rule: subtyping for functions is opposite (or contra-) the subtyping for their arguments. We can put the covariant return type and contravariant argument type principles together to get the general subtyping rule for functions: A2 <: A1 R1 <: R2 -------------------- subtype-function A1 -> R1 <: A2 -> R2