TypeScript: Best Common Type

TypeScript tries to find the best common type when a type inference is made from several expressions.

Let’s experiment a bit with following types. Square is a subtype of Shape and Circle is also a subtype of Shape.

class Shape {
    color: string;
}

class Square extends Shape {
    sideLength: number;
}

class Circle extends Shape {
	radius: number;
}

The type of xs1 is Shape[] because Shape is the common supertype of Shape, Square and Circle. However, surprisingly the type of xs2 is not Shape[] though Shape is the common supertype of Square and Circle. Here the type of xs2 is (Circle | Square)[] because there is no object that is strictly of type Shape in the array.

var xs1 = [new Shape(), new Square(), new Circle()];
var xs2 = [new Square(), new Circle()];

You can override this type by annotating the type explicitly with Shape[].

var xs3: Shape[] = [new Square(), new Circle()];

Now let’s see how the return type of a function is inferred when the function returns two values with different types.

function createShape1(square: boolean) {
    if (square) {
        return new Square();
    } else {
        return new Shape();;
    }
}

The return type of createShape1 function is Shape as expected because Shape is the common supertype of Square and Shape.

function createShape2(square: boolean) { // Type Error
    if (square) {
        return new Square();
    } else {
        return new Circle();
    }
}

However, if we change the return value of else branch to new Circle(), createShape2 function no longer type checks. TypeScript complains about “error TS2354: No best common type exists among return expressions.” It fails to find the best common type though it can be inferred as either Square|Circle or Shape.

We can satisfy the type checker by giving an explicit type. The return type can be either Square | Circle or Shape.

function createShape3(square: boolean): Square | Circle {
    if (square) {
        return new Square();
    } else {
        return new Circle();
    }
}

function createShape4(square: boolean): Shape {
    if (square) {
        return new Square();
    } else {
        return new Circle();
    }
}

From the examples above, we can see that the type inference rule of TypeScript is not orthogonal. When you mix multiple types in an array, TypeScript finds the best common type that is the union of the types of the element expressions. But when a function mixes multiple return types in return values, it finds the best common type that is the first supertype of each of the others.

You can see the difference in the type inference rules taken from the TypeScript 1.4 specification.

Array Type

  • If the array literal is empty, the resulting type is an array type with the element type Undefined.
  • Otherwise, if the array literal is contextually typed by a type that has a property with the numeric name ‘0’, the resulting type is a tuple type constructed from the types of the element expressions.
  • Otherwise, the resulting type is an array type with an element type that is the union of the types of the element expressions.

Function Return Type

  • If there are no return statements with expressions in f’s function body, the inferred return type is Void.
  • Otherwise, if f’s function body directly references f or references any implicitly typed functions that through this same analysis reference f, the inferred return type is Any.
  • Otherwise, if f is a contextually typed function expression (section 4.9.3), the inferred return type is the union type (section 3.4) of the types of the return statement expressions in the function body, ignoring return statements with no expressions.
  • Otherwise, the inferred return type is the first of the types of the return statement expressions in the function body that is a supertype (section 3.10.3) of each of the others, ignoring return statements with no expressions. A compile-time error occurs if no return statement expression has a type that is a supertype of each of the others.
Advertisements

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.

TypeScript: More on Union Types

In the previous article, I explained the basics of union types in TypeScript.

Though union types resemble sum types of algebraic data types, they are different types. (Confusingly, sum types are sometimes also called “unions” or “disjoint unions” in some languages.). The notion of union types, A|B denotes the ordinary union of the set of values belonging to A and the set of values belonging to B, with no added tag to identify the origin of a given element.

With this in mind, let’s look at some properties of TypeScript union types:

  • Identity: A|A is equivalent to A
  • Commutativity: A|B is equivalent to B|A
  • Associativity: (A|B)|C is equivalent to A|(B|C)
  • Subtype collapsing: A|B is equivalent to A if B is a subtype of A

In sum types, A|A is different from A because it is the sum of type A and A. (either left A or right A). However, in union types, A|A is equivalent to A because there is no tag attached to make left and right ‘A’ distinct. So it is also called “non-disjoint union types” to differentiate it from “disjoint union types” of ADT.

Non-disjoint union types are different from disjoint union types in that it lacks any kind of case construct. If v is of type A|B, the only members that we can safely access on v are members that belong to both A and B. This is why we can access only weight and gears members of transport object.

interface Car {
    weight: number;
    gears: number;
    type: string;
}
interface Bicycle {
    weight: number;
    gears: boolean;
    size: string;
}
var transport: Car|Bicycle = /* ... */;
console.log(transport.type); // Error, transport does not have 'type' property

So if the constituent types of a union type have no members in common, there is nothing we can do on the union type. For example, if x is of number|string|boolean type, we can only access toString and valueOf members that are common across these three types.

Wait a minute! I’ve just mentioned that union types lack any kind of case construct. But what about the type guards of TypeScript? Isn’t it a kind of case construct that can identify the origin of an element? The answer is both yes and no. Yes, you can inspect the runtime type of a union type with type guards. But no because there is nothing special that prevents you from checking non-constituent types. For example, the following code is perfectly okay in TypeScript:

var x: number | boolean;
if (typeof x === 'number') {
    // ...
}

x is either number or boolean, but the type guard here checks if the type of x is number. In the body of if expression, TypeScript infers that the type of x is number though it never can’t be of number type. On the contrary, in programming languages with sum types, we can check only the constituent types with pattern matching.

TypeScript Function Overloads

In TypeScript, you can declare an overloaded function by declaring the function with a multiple invocation signatures.

interface Foo
{
    bar: {
        (s: string): number;
        (n: number): string;
    }
}

or

interface Foo {
    bar(s: string): number;
    bar(n: number): string;
}

The type checker of TypeScript works as expected:

var foo1: Foo = ...;

var n: number = foo1.bar('hello world'); // OK
var s: string = foo1.bar(123);           // OK
var a: boolean = foo1.bar(false);        // ERROR

However, you can’t actually define an overloaded function with multiple function definitions as in C# or Java. The compiler complains about “error TS2393: Duplicate function implementation”. This happens because the signatures of both bar functions are identical once their types are erased.

class FooClass implements Foo {
    bar(s: string): number {
        return s.length;
    }
	
    bar(n: number): string {
        return n.toString();
    }
}

is translated to

var FooClass = (function () {
    function FooClass() {
    }
    FooClass.prototype.bar = function (s) {
        return s.length;
    };
    FooClass.prototype.bar = function (n) {
        return n.toString();
    };
    return FooClass;
})();

To define an overloaded function in TypeScript, the function must be singular and perform appropriate dispatching internally using type guards.

class FooClass implements Foo
{
    public bar(s: string): number;
    public bar(n: number): string;
    public bar(arg: string | number): any 
    {
        if (typeof arg=== 'number')
            return arg.toString();
        if (typeof arg === 'string')
            return arg.length;
    }
}

When you declare a variable with FooClass, you can’t see bar(arg: string | number) method even though it is declared as public. It is automatically hidden by the more specifically typed overloads.

TypeScript’s function overloads are a bit more flexible than those of C# or Java in that an overloaded function can have different return types. Note that bar(arg: string) returns number while bar(arg: number) returns string.

TypeScript Type System: Interface Types

In JavaScript, it is common to add more members to existing objects by monkey patching. For example, jQuery UI provides its API by adding a bunch of members to the jQuery object provided by jQuery.

Interface types in conventional programming languages such as Java and C# can’t represent this idiom because interfaces can’t be modified once they are declared. So if TypeScript uses the same interface types as those languages, it could be a severe limitation to the usability of the language.

Fortunately, the designers of TypeScript language were clever enough to come up with an idea to represent this idiom in a type safe manner. This idea is simply to make interface types open, meaning you can add your own members later by simply writing another interface block with the same name.

For example, Box interface has width and height properties. But you can add scale property later by writing another interface block with scale property.

interface Box {
	width: number;
	height: number;
}

interface Box {
	scale: number;
}

var b1: Box = { width: 200, height: 180, scale: 1.0 };
var b2: Box = { width: 400, height: 360 }; // Error because scale is missing

Let’s get back to the jQuery UI example mentioned above. jQuery library declares JQuery interface initially. Later, jQuery UI also declares JQuery interface and add more members to the interface. TypeScript compiler merges these two interfaces into one JQuery interface with members from both interfaces.

/**
 * The jQuery instance members
 */
interface JQuery {
    /**
     * Register a handler to be called when Ajax requests complete. This is an AjaxEvent.
     *
     * @param handler The function to be invoked.
     */
    ajaxComplete(handler: (event: JQueryEventObject, XMLHttpRequest: XMLHttpRequest, ajaxOptions: any) => any): JQuery;
    // ...
}
interface JQuery {

    accordion(): JQuery;
    accordion(methodName: 'destroy'): void;
    // ...
}

TypeScript expands this idea to a more general concept called Declaration Merging. Refer to TypeScript Handbook for more information on merging modules and merging modules with classes/functions/enums.

Setting a TypeScript Compiler Project

TypeScript is not just a standalone compiler. It provides the Compiler API and the Language Service API so that you can create your own TypeScript tools leveraging all the powerful features of TypeScript compiler.

This article will show you how to setup a project using the compiler API of the latest TypeScript compiler.

Add src/tsconfig.json

To create a TypeScript project, the first thing we need to do is to create a file named tsconfig.json. The tsconfig.json file specifies the root files and the compiler options required to compile the project.

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "target": "ES5",
        "out": "../build/tsstyle.js",
        "sourceMap": true
    },
    "files": [
        "tsstyle.ts"
    ]
}

Add package.json

Then add the project name, version, description and dependencies to package.json file. The following package.json gets the latest version of TypeScript from the GitHub repo. It also adds gulp as a dependency as we will use gulp as our build system.

{
  "name": "tsstyle",
  "version": "0.1.0",
  "description": "tsstyle",
  "dependencies": {
    "gulp": "^3.9.0"
  },
  "devDependencies": {
    "gulp-typescript": "*",
    "typescript": "Microsoft/TypeScript"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Kwang Yul Seo"
}

Add gulpfile.js

The next thing we need to do is to create a build script. Setup gulpfile.ts as in the following to use the build settings of tsconfig.json we created above.

var gulp = require('gulp');
var ts = require('gulp-typescript');

var tsProject = ts.createProject('src/tsconfig.json', 
    { typescript: require('typescript') });

gulp.task('default', function() {
    var tsResult = tsProject.src()
        .pipe(ts(tsProject));

    return tsResult.js.pipe(gulp.dest('built/local'));
});

{ typescript: require('typescript') } makes sure that we use the latest version of TypeScript from the GitHub repo instead of the TypeScript compiler bundled with gulp.

Add type definitions

Use tsd to add type definitions.

tsd query node --action install

Implementation: src/tsstyle.ts

Add references to type definition files and import typescript module. Start implementing the code. Refer to Traversing the AST with a little linter for details.

/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../node_modules/typescript/bin/typescript.d.ts" />

import {readFileSync} from "fs";
import * as ts from "typescript";

/// Code here

Visual Studio Code Integration

Add the following tasks.json file to .settings directory. When you run build task, VS Code will run gulp.

{
    "version": "0.1.0",
    "command": "gulp",
    "isShellCommand": true,
    "tasks": [
        {
            "taskName": "default",
            // Make this the default build command.
            "isBuildCommand": true,
            // Show the output window only if unrecognized errors occur.
            "showOutput": "silent",
            // use the standard tsc problem matcher to find compile problems
            // in the output.
            "problemMatcher": "$tsc"
        }
    ]
}

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.