Thoughts on Intersection Types

Facebook’s Flow is a static type checker, designed to find type errors in JavaScript programs. Flow’s type system is similar to that of TypeScript in that it is a gradual type system and relies heavily on type inference to find type errors.

One interesting feature of Flow that is missing in TypeScript is intersection types. For example, in Flow you can represent a function which takes either Bar or Foo with intersection types.

/* @flow */
class Foo {}
class Bar {}
declare var f: ((x: Foo) => void) & ((x: Bar) => void);
f(new Foo());
f(true); // Flow will error here.

The function f has type ((x: Foo) => void) & ((x: Bar) => void). The notation A & B represents the intersection type of A and B, meaning a value of A & B belongs to both A and B. We can capture this intuition with the following sub-typing rules:

[1] A & B <: A
[2] A & B <: B
[3] S <: A, S <: B ~> S <: A & B

In Flow, intersection types are primarily used to support finitary overloading. func takes either number or string and returns nothing. This can be represented as intersection of two function types as in the following:

type A = (t: number) => void & (t: string) => void
var func : A;

However, TypeScript doesn’t need intersection types to support finitary overloading because it directly supports function overloading.

interface A {
  (t: number): void;
  (t: string): void;
}
var func: A

Or in TypeScript 1.4, you can represent the same function more succinctly with union types.

interface A {
  (t: number | string): void;
}
var func: A

This is not a coincidence! Intersection types are the formal dual of union types. When an arrow is distributed over a union, the union changes to an intersection.

(A -> T) & (B -> T)  <:  (A | B) -> T

So it means the following two types are equivalent:

  • (t: number | string): void
  • (t: number) => void & (t: string) => void

So far intersection types seem redundant once a language have union types (or vice versa). However, there are some real world use cases where intersection type are actually required.

Let’s check how ES6’s new function Object.assign is typed in TypeScript (lib.core.es6.d.ts).

interface ObjectConstructor {
    /**
      * Copy the values of all of the enumerable own properties from one or more source objects to a 
      * target object. Returns the target object.
      * @param target The target object to copy to.
      * @param sources One or more source objects to copy properties from.
      */
    assign(target: any, ...sources: any[]): any;
    ...
}

Currently, both the argument types and the return type of Object.assign is any because the type system of TypeScript can’t represent the type correctly. Let’s try to type this function correctly. First, we can make this function polymorphic by assigning A to target and B[] to sources.

Object.assign<A,B>(target: A, ...sources: B[]): ?;

Okay. We are now stuck with the return type. The return value has all the properties of both A and B. It means with structural typing, the return value is a subtype of both ‘A’ and ‘B’ (belongs to both A and B). Yeah! We need intersection types to represent this value. So the correct signature of this function is:

Object.assign<A, B>(target: A, ...sources: B[]): A & B;

The same reasoning also applies to mixins.

mixins<A,B>(base: { new() :A }, b: B}) : { new(): A & B} 

TypeScript developers are still discussing on adding intersection types. Many people think that there are not enough compelling use cases. Also intersecting two primitive types like string & number makes no sense, which makes intersection types less desirable.

I think adding intersection types to TypeScript makes the language more consistent because these two concepts are the dual of each other and can’t really be separable. But I also think that the language design must be conservative. So let’s just wait until we have more compelling reasons to add intersection types.

Advertisements

TypeScript Type System: Union Types

Many JavaScript libraries support taking in values of more than one type. For example, jQuery’s height and width properties take an integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string).

Without union types, TypeScript definition files must represent this property as type any, effectively losing type safety. This shortcoming could be worked around in function overloads as TypeScript supports function overloads (multiple functions with same name but different argument types). However, there is no equivalent for object properties, type constraints, or other type positions.

Union types let you represent a value which is one of multiple types. You can specify the types using the new | operator.

function process(n: string|number) { /* ... */ }
process('foo'); // OK
process(42); // OK
process($('div')); // Error

In the function body of process, you can examine the type of an expression using typeof or instanceof operator. TypeScript understands these conditions and change type inference accordingly.

function process(n: string|number) {
  if (typeof n === 'number') {
    // Error because n is a number.
    console.log(n.toLowerCase());
  }
}

So far, Union types look a lot like a sum type in algebraic data type. Type A|B represents a type either A or B and you use type guards to examine the type.

However, Union types of TypeScript have more interesting properties combined with structural typing. For example, the type A|B has a property P of type X|Y if A has a property P of type X and B has a property P of type Y. Here an example taken from Spec Preview: Union types.

interface Car {
  weight: number;
  gears: number;
  type: string;
}
interface Bicycle {
  weight: number;
  gears: boolean;
  size: string;
}
var transport: Car|Bicycle = /* ... */;
var w: number = transport.weight; // OK
var g = transport.gears; // OK, g is of type number|boolean

console.log(transport.type); // Error, transport does not have 'type' property
console.log((<Car>transport).type); // OK

Because both Car and Bicycle types have weight property of number type, you can access weight property of Car|Bicycle type without using a type guard. gears property is more interesting. The type of gears is number in Car and boolean in Bicycle, so the type of gears in Car|Bicycle becomes number|boolean.

Union types also change the algorithm to find the best common type. For example, now TypeScript can infer the type of a mixed array:

var xs = [1,2,3, "hello", "world"];

In old version of TypeScript, the type of xs is {}[] because number and string have nothing in common. But with Union types, the type of xs is inferred as string|number.

In conclusion, Union types provide a succinct way to represent a value with multiple types. They help you keep “DRY principle” by removing all redundant declarations of functions, properties and type constraints.