The Definitive TypeScript Guide

By on December 31, 2013 3:19 pm

This article describes the features and functionality of TypeScript 2.2.

One of the newer and most interesting languages for large-scale RIA development is Microsoft’s TypeScript. TypeScript’s claim to fame is that it adds optional static typing, interfaces, and modern ECMAScript 6+ (ES6+) features to JavaScript. In comparison to similar languages like CoffeeScript and Dart, TypeScript is unique because it is written as a superset of JavaScript—in other words, all JavaScript is already TypeScript. The language and compiler are open source and written in JavaScript, and the TypeScript team have taken great care to align the language’s extra features as closely as possible with what’s available in ES6 and later, so it also provides developers with an easy path to start using many of these newer language features.

While TypeScript’s documentation has improved significantly since this guide was first posted, this Definitive Guide still provides one of the best overviews of the key features of TypeScript, assuming you already have a reasonable knowledge of JavaScript and a basic understanding of how class-based inheritance works (as in Java, PHP, C#, etc.). The guide is regularly updated to provide new information about the latest versions of TypeScript.

Very narrow sections of TypeScript which are necessary to support legacy approaches to writing JavaScript but which are inappropriate when writing modern, portable code are excluded from this guide (declaration merging and namespaces).

Installation and usage

Installing TypeScript is as simple as running npm install typescript. Once installed, the TypeScript compiler is available by running tsc or running a local task to automatically compile after each file is saved. If you want to try out TypeScript in your browser, the TypeScript Playground lets you experience TypeScript with a full code editor, with the limitation that modules cannot be used. Most of the examples in this guide can be pasted directly into the playground to quickly see how TypeScript is compiled into easy-to-read JavaScript.

Using let and const

ES6 introduces two new ways to declare variables using let and const. Declaring variable using let or const is almost always preferable to using var because var declarations have some unusual scoping rules compared to other languages. Unlike variables declared with var, those declared with let are “block-scoped” variables which are not visible outside of their nearest containing block or for loop. This helps avoid unintended collisions from reusing variable names. Variables declared using const are scoped similarly to those using let — the big difference is that it is a compile-time error if you to try to reassign the value of a const. You can however still change the properties within an object held by a const variable. Using const when possible helps a programmer signal their intent about how such variables will behave — which makes code easier to understand.

TypeScript 2.0 added the readonly keyword which disallows reassignment and implies a non-writable property or a property with only a get accessor. It does not mean non-primitives are immutable.

Imports and exports

In order to start talking about how to write TypeScript, we first need to understand how to create and load TypeScript files. TypeScript files use the .ts file extension, and like both AMD and CommonJS, each TypeScript file nominally represents a single module. Importing modules in TypeScript follows the ES Modules (ESM) API:

import myModule from './myModule';

Be default, module path resolution for relative module IDs is the same in TypeScript as it is in AMD and CommonJS, where the module ID is resolved relative to the directory containing the referencing module. Absolute module IDs work slightly differently; first, if there is a matching ambient module declaration, its value is used as-is. Otherwise, the compiler walks up the filesystem, starting from the directory containing the referencing module, looking for <moduleId>.ts, then <moduleId>.d.ts, in each parent directory, until it finds a match.

Prior to TypeScript 2.0, support existed for two ways of resolving module names: classic (a module name always resolves to a file, modules are searched using a folder walk) and node (uses rules similar to the Node.js module loader). Unfortunately neither approach solves the approach of defining modules relative to a baseUrl, which is what AMD systems such as Dojo and RequireJS, and SystemJS use.

Instead of introducing a third type of module resolution for TypeScript 2.0, the TypeScript team added configuration settings to solve this within the existing systems: baseUrl, paths, and rootDirs.

paths may only be used if baseUrl is set. If at least one of these properties is defined then the TypeScript compiler will try to use it to resolve module names and if it fails, it will fallback to a default strategy.

Exporting values from a module can be done with the export keyword:

export function foo() {}
export let bar = 123;
export class MyClass {}

Importing the entire module using star (*) will cause the module’s exports to be available locally with the same names: foo, bar, and MyClass. To use these values from another module, simply import the module and access its exported properties:

import * from './myModule';
foo();
bar + 5; // = 128
new MyClass();

To import individual properties surround the property names with curly braces:

import { foo } from './myModule';
import { bar, MyClass } from './myModule';
foo();
bar + 5; // = 128
new MyClass();

You can specify a default export by adding the default keyword immediately after export:

export default class MyClass {}
export someStaticMethod()

This is equivalent to returning a value from an AMD factory function, or assigning a value to module.exports in CommonJS. To use the value, you can simply import and use it directly:

import MyClass from './MyClass';
let myInstance = new MyClass();
MyClass.someStaticMethod();

Providing an import identifier with no curly braces will load the default import, and it will be implicitly aliased to whatever you specify. Other imports can be aliased using the as keyword:

// AnotherClass = MyClass from MyClass.ts
import AnotherClass from './MyClass';
import { foo, bar as baz } from './myModule';

To attach a module’s exports to a named property, like when you assign properties to the AMD or CommonJS exports object, provide an alias to the star import:

import * as foo from './myModule';
foo.foo();
foo.bar;
new foo.MyClass();

Note that when using mixins with TypeScript classes (2.2+), there are some subtle details which are described under the section about classes.

Language sugar

Before diving into the static typing features of TypeScript, it’s essential to review some of the general enhancements made to functions in TypeScript, since some of these changes make features of the type system easier to understand.

TypeScript includes four major improvements to functions: optional parameters, default argument values, rest parameters, and arrow functions.

Optional parameters can now be defined by suffixing a parameter identifier with a question mark:

function getRange(max, min, exclusive?) {
  // ...
}

Here, exclusive is an optional parameter. This is meaningless when talking about JavaScript, since all parameters are always optional, but in TypeScript, the compiler prohibits omitting typed arguments unless they are specified as optional or have a default value.

An optional parameter is essentially just a shorthand for specifying undefined as the default value for a parameter. Making a parameter optional with a different value as the default is as simple as assigning it within the parameters list instead of using the question mark shorthand:

function getRange(max, min = 0, exclusive = false) {
  // ...
}

In this case, min is optional and will default to 0, and exclusive is optional and will default to false.

TypeScript also adds support for a final variadic ...rest parameter, which collects any extra arguments passed to the function into a single named array:

function publish(topic, ...args):void {
  // ...
}

In this case, calling publish('/foo', 'a', 'b', 'c') would cause topic to be a string '/foo' and args to be an array [ 'a', 'b', 'c' ]. Note that using this feature adds an extra loop to your function that runs on each call to collect arguments into the rest parameter, so performance-critical code should continue to operate directly against the arguments object instead.

TypeScript 2.1 added support for ES8 rest parameters within objects.

TypeScript also includes support for the arrow function from ES6. This new function type provides a new shorthand syntax, and also changes the way the this keyword works, so its value is taken from the nearest lexical scope rather than the caller context object like regular JavaScript functions:

let obj = {
  arrow: () => {
    console.log(this);
  },
  regular: function () {
    console.log(this);
  }
};

obj.arrow(); // logs `window`
obj.regular(); // logs `obj`

TypeScript also includes object shorthands that reduce the amount of code needed for common operations with object literals:

const foo = 'foo';
let a = {
  // shorthand identifier reference, equivalent to `foo: foo`
  foo,

  // shorthand method, equivalent to `bar: function () {}`
  bar() {

  }
};

Destructuring

Multiple variables can be assigned to directly from an array:

let x, y;
[x, y] = [10, 20];

Which can be shortened:

let [x, y] = [10, 20];

Destructuring works with objects as well:

let { place, location: [x, y] } = { place: 'test', location: [10, 20] };
// local variable 'place' = 'test'
// local variable 'x' = 10
// local variable 'y' = 20

Other language features

TypeScript also includes multiple other features from current or future ECMAScript specifications, including:

Types

Without adding any type hinting, variables in TypeScript are of the any type, which means they are allowed to contain any type of data, just like JavaScript. The basic syntax for adding type constraints to code in TypeScript looks like this:

    function toNumber(numberString: string): number {
    const num: number = parseFloat(numberString);
    return num;
}

In the above code, the toNumber function accepts one parameter that must be a string, and it returns a number. The variable num is explicitly typed to contain a number (though TypeScript is smart enough to know that the standard parseFloat function returns a number and therefore that num is a number since it is assigned at the time it is declared). The primitive types that TypeScript provides match the primitive types of JavaScript itself: any, number, string, boolean,void (i.e. null or undefined), and as of TS 2.0, never. In most cases, never is inferred in functions where the code flow analysis detects unreachable code and as a developer you don’t have to worry about it. For example, if a function only throws, it will get a never type.

When writing an expression (function call, arithmetic operation, etc.), you can also explicitly assert the resulting type of the expression, which is necessary if you are calling a function where TypeScript cannot figure out the return type automatically. For example:

function numberStringSwap(value: any, radix: number = 10): any {
    if (typeof value === 'string') {
        return parseInt(value, radix);
    }
    else if (typeof value === 'number') {
        return String(value);
    }
}

let num = <number> numberStringSwap('1234');

In this example, the return value of numberStringSwap is ambiguous (the any type) because the function might return more than one type. In order to remove the ambiguity, the type of the expression being assigned to num on the last line is being explicitly asserted by prefixing the call expression with <number>. This can be done anywhere, so long as the type being asserted is compatible with the type of the expression; in other words, if TypeScript knew that numberStringSwap returned a number on line 10, attempting to assert <string> would result in a compiler error (“Cannot convert number to string”) since the two types are known to be incompatible.

When writing code in TypeScript, it is a good idea to explicitly add types to most of your variables and functions, even if the compiler can resolve the implicit types (e.g. let foo = 'a';), since doing so makes it easier for humans reading your code to know at a glance exactly what is intended. When compiling, setting "noImplicitAny": true in the tsconfig.json compilerOptions section will also prevent any accidental implicit any types from sneaking into your code (i.e. areas where the compiler is not smart enough to figure out the correct type).

TypeScript 1.8 also added support for string literal types. These are useful when you know that the value of a parameter can match one of a list of strings, e.g. easing: "ease-in" | "ease-out" | "ease-in-out";.

Local class, interface, enum, and type alias declarations may also appear inside function declarations (TS 1.6+). Scoping for local types is blocked, similar to variables declared with let and const.

Object types

In addition to the five primitive types, TypeScript allows complex types (like objects and functions) to be easily defined and used as type constraints. Just as object literals are at the root of most object definitions in JavaScript, the object type literal is at the root of most object type definitions in TypeScript. In its most basic form, it looks very similar to a normal JavaScript object literal:

let point: {
  x: number;
  y: number;
};

In this example, the point variable is defined as accepting any object with x and y properties. Note that, unlike a JavaScript object literal, the object type literal separates fields using semicolons, not commas.

When TypeScript compares two different object types to decide whether or not they match, it does so structurally. This means that rather than compare types by checking whether or not they both inherit the same base constraint object (like instanceof), the properties of each object are compared. As long as a given object has all of the properties that are required by the constraint on the variable being assigned to, they are considered compatible (although object literal assignments are treated more strictly as a special case in TS 1.6+):

let point: { x: number; y: number; };

point = { x: 0, y: 0 };
// OK, properties match
point = { x: 'zero', y: 0 };
// Error, `x` property type is wrong
point = { x: 0 };
// Error, missing required property `y`
point = { x: 0, y: 0, z: 0 };
// Error in TS 1.6+, object literal may only specify known properties
const otherPoint = { x: 0, y: 0, z: 0 };
point = otherPoint;
// OK, extra properties not relevant for non-literal assignment

In order to reduce type duplication, the typeof operator can also be used to define a type constraint. For instance, if we were to add a point2 variable, instead of having to write this:

let point: { x: number; y: number; };
let point2: { x: number; y: number; };

We could instead simply reference the type of point using typeof:

let point: { x: number; y: number; };
let point2: typeof point;

This mechanism helps to reduce the amount of code we need to reference the same type, but there is another even more powerful abstraction in TypeScript for reusing object types: interfaces. An interface is, in essence, a named object type literal. Changing the previous example to use an interface would look like this:

interface Point {
  x: number;
  y: number;
}

let point: Point;
let point2: Point;

This change allows the Point type to be used in multiple places within the code without having to redefine the type’s details over and over again. Interfaces can also extend other interfaces or classes using the extends keyword in order to compose more complex types out of simple types:

interface Point3d extends Point {
  z: number;
}

In this example, the resulting Point3d type would consist of the x and y properties of the Point interface, plus the new z property.

Methods and properties on objects can also be specified as optional, in the same way that function parameters can be made optional:

interface Point {
  x: number;
  y: number;
  z?: number;
}

Here, instead of specifying a separate interface for a three-dimensional point, we simply make the z property of the interface optional; the resulting type checking would look like this:

let point: Point;

point = { x: 0, y: 0, z: 0 };
// OK, properties match
point = { x: 0, y: 0 };
// OK, properties match, optional property missing
point = { x: 0, y: 0, z: 'zero' };
// Error, `z` property type is wrong

So far, we’ve looked at object types with properties, but haven’t specified how to add a method to an object. Because functions are first-class objects in JavaScript, it’s possible to use the property syntax, but TypeScript also provides a shorthand syntax for specifying methods, which becomes very convenient later when we start working with classes:

interface Point {
  x: number;
  y: number;
  z?: number;
  toGeo(): Point;
}

In this example, we’ve added a toGeo method to the Point interface, which accepts no arguments and returns another Point object. Like properties, methods can also be made optional by putting a question mark after the method name:

interface Point {
  // ...
  toGeo?(): Point;
}

Objects that are intended to be used as hash maps or ordered lists can be given an index signature, which enables arbitrary keys to be defined on an object:

interface HashMapOfPoints {
  [key: string]: Point;
}

In this example, we’ve defined a type where any key can be set, so long as the assigned value is of type Point. As in JavaScript, it is only possible to use string or number as the type of the index signature.

For object types without an index signature, TypeScript will only allow properties to be set that are explicitly defined on the type. If you try to assign to a property that doesn’t exist on the type, you will get a compiler error. Occasionally, though, you do want to add dynamic properties to an object without an index signature. To do so, you can simply use array notation to set the property on the object instead: a['foo'] = 'foo';. Note, however, that using this workaround defeats the type system for these properties, so only do this as a last resort.

Note that as of TypeScript 2.2, property (dotted) access syntax for types with string index signatures is now allowed. This change normalizes type checking behavior between foo.bar and foo['bar']. Previously there were scenarios that did not work as expected with dotted access, which should clean up the need for some unnecessary things like:

contentStyles['transform'] = `...`;

could now be written as:

contentStyles.transform = `...`;

The potential downside with this change that we need to investigate further is that anything that has an index type and declared properties will not receive a compilation error if you typo a known property.

object type (TS 2.2+)

TypeScript 2.2 adds the the object type fixes a previous limitation in defining a type definition where something can be an Object or a non-primitive type. This was not possible to handle with previous versions of TypeScript because number, string, and boolean could all be assigned to Object. So the new object type (note the lowercase) implies a type that is assignable to Object, except for primitives.

Tuple types

While JavaScript itself doesn’t have tuples, TypeScript makes it possible to emulate typed tuples using Arrays. If you wanted to store a point as an (x, y, z) tuple instead of as an object, this can be done by specifying a tuple type on a variable:

let point: [ number, number, number ] = [ 0, 0, 0 ];

Tuple types cannot be written as interfaces and they cannot have optional values, which limits their usefulness. Still, if you are working with code that emulates tuples using Arrays in JavaScript, now you can successfully type these objects in TypeScript.

Function types

Because functions in JavaScript are first-class objects, the object type literal syntax can also be used to specify that an object is supposed to be a function. To do this, the same method syntax as shown above for toGeo is used, but with the method name left blank:

let printPoint: {
  (point: Point): string;
};

Here, printPoint is defined as accepting a function that takes a point object and returns a string.

Because functions are so common in JavaScript, there is a specific function type shorthand syntax in TypeScript that can be used to define functions with a single call signature:

let printPoint: (point: Point) => string;

Note the use of the arrow (=>), which comes from the ES6 arrow function, to define the return type of the function instead of a colon. Colons (:) are used when defining the return type of a method defined within an object type literal, interface, or class, whereas arrows are used by the function type shorthand shown here. This is a little confusing at first, but as you work with TypeScript, you will find it is easy to know when one or the other should be used. For instance, in the above example, using a colon would look wrong because it would result in two colons directly within the constraint: let printPoint:(point:Point):string.

Now that we know the function type syntax, going back to our Point definition, defining toGeo as a property instead of a method looks like this:

interface Point {
  x: number;
  y: number;
  z?: number;
  toGeo: () => Point;
}

Functions can also be typed as constructors by putting the new keyword before the function type:

let Point: { new (): Point; };
let ShorthandEquivalent: new () => Point;

In this example, any function assigned to Point or ShorthandEquivalent would need to be a constructor that creates Point objects.

Because the object literal syntax allows us to define objects as functions, it’s also possible to define function types with static properties or methods (like the JavaScript String function, which also has a static method String.fromCharCode):

let Point: {
  new (): Point;
  fromLinear(point: Point): Point;
  fromGeo(point: Point): Point;
};

Here, we’ve defined Point as accepting a constructor that also needs to have static Point.fromLinear and Point.fromGeo methods. The only way to actually do this is to define a class that implements Point and has static fromLinear and fromGeo methods; we’ll look at how to do this later when we discuss classes in depth.

Overloaded functions

Earlier, we created an example numberStringSwap function that converts between numbers and strings:

function numberStringSwap(value: any, radix: number):any {
    if (typeof value === 'string') {
        return parseInt(value, radix);
    }
    else if (typeof value === 'number') {
        return String(value);
    }
}

In this example, ambiguity exists in the call signature because the return type is any. However, we know that this function returns a string when it is passed a number, and a number when it is passed a string. To help the TypeScript compiler know what we know, we can use function overloads to eliminate the call signature ambiguity and restore type checking on the return value.

One way to write the above function, in which typing is correctly handled, is:

function numberStringSwap(value: number, radix?: number): string;
function numberStringSwap(value: string): number;
function numberStringSwap(value: any, radix: number = 10): any {
    if (typeof value === 'string') {
        return parseInt(value, radix);
    }
    else if (typeof value === 'number') {
        return String(value);
    }
}

With the extra call signatures, TypeScript now knows that when the function is passed a string, it returns a number, and vice-versa. In TypeScript 1.4+, you can also use union types in some cases instead of function overloads, which will be discussed later in this guide.

It is extremely important to keep in mind that the concrete function implementation must have an interface that matches the lowest common denominator of all of the overload signatures. This means that if a parameter accepts multiple types, as value does here, the concrete implementation must specify a type that encompasses all the possible options. In the case of numberStringSwap, because string and number have no common base, the type for value must be any.

Similarly, if different overloads accept different numbers of arguments, the concrete implementation must be written such that any arguments that do not exist in all overload signatures are optional. For numberStringSwap, this means that we have to make the radix argument optional in the concrete implementation. This is done by specifying a default value.

Not following these rules will result in a generic “Overload signature is not compatible with function definition” error.

Note that even though our fully defined function uses the any type for value, attempting to pass another type (like a boolean) for this parameter will cause TypeScript to throw an error because only the overloaded signatures are used for type checking. In a case where more than one signature would match a given call, the first overload listed in the source code will win:

function numberStringSwap(value: any): any;
function numberStringSwap(value: number): string;

numberStringSwap('1234');

Here, even though the second overload signature is more specific, the first will be used. This means that you always need to make sure your source code is properly ordered so that your preferred overloads are listed first.

Function overloads also work within object type literals, interfaces, and classes:

let numberStringSwap: {
  (value: number, radix?: number): string;
  (value: string): number;
};

Note that because we are defining a type and not creating an actual function declaration, the concrete implementation of numberStringSwap is omitted.

TypeScript also allows you to specify different return types when an exact string is provided as an argument to a function. For example, TypeScript’s ambient declaration for the DOM’s createElement method looks like this:

createElement(tagName: 'a'): HTMLAnchorElement;
createElement(tagName: 'abbr'): HTMLElement;
createElement(tagName: 'address'): HTMLElement;
createElement(tagName: 'area'): HTMLAreaElement;
// ... etc.
createElement(tagName: string): HTMLElement;

This means, in TypeScript, when you call e.g. document.createElement('video'), TypeScript knows the return value is an HTMLVideoElement and will be able to ensure you are interacting correctly with the DOM Video API without any need to type assert.

Generic types

TypeScript includes the concept of a generic type, which can be roughly thought of as a type that must include or reference another type in order to be complete. Two very common generic types that you will run into are Array and Promise.

The syntax of a generic type is GenericType<SpecificType>. For example, an “array of strings” type would be Array<string>, and a “promise that resolves to a number” type would be Promise<number>. Generic types may require more than one specific type, like Converter<TInput, TOutput>, but this is extremely uncommon. The placeholder types inside the angle brackets are called type parameters. Unlike non-generic object types, generic types can only be created as interfaces or classes.

Since arrays are the most common type of generic type, it is easiest to explain how to create your own generic types using an array-like interface as an example:

interface Arrayish<T> {
  map<U>(callback: (value: T, index: number, array: Arrayish<T>) => U, thisArg?: any): Array<U>;
}

In this example, Arrayish is defined as a generic type with a single map method, which corresponds to the Array#map method from ECMAScript 5. The map method has a type parameter of its own, U, which is used to indicate that the return type of the callback function needs to be the same as the return type of the map call.

Actually using this type would look something like this:

const arrayOfStrings: Arrayish<string> = [ 'a', 'b', 'c' ];

const arrayOfCharCodes: Arrayish<number> = arrayOfStrings.map(function (value: string): number {
  return value.charCodeAt(0);
});

Here, arrayOfStrings is defined as being an Arrayish containing strings, and arrayOfCharCodes is defined as being an Arrayish containing numbers. We call map on the array of strings, passing a callback function that returns numbers. If the callback were changed to return a string instead of a number, the compiler would raise an error that the types were not compatible, because arrayOfCharCodes is explicitly typed and the use of a type parameter for the return value of the callback ensures that the compiler can determine compatibility.

Because arrays are an exceptionally common generic type, TypeScript provides a shorthand just for arrays: SpecificType[]. Note, however, ambiguity can occasionally arise when using this shorthand. For example, is the type () => boolean[] an array of functions that return booleans, or is it a single function that returns an array of booleans? The answer is the latter; to represent the former, you would need to write Array<() => boolean> or { (): boolean; }[].

TypeScript also allows type parameters to be constrained to a specific type by using the extends keyword within the type parameter, like interface PointPromise<T extends Point>. In this case, only a type that structurally matched Point could be used with this generic type; trying to use something else, like string, would cause a type error.

Union types

The union type is used to indicate that a parameter or variable can contain more than one type. For example, if you wanted to have a convenience function like document.getElementById that also allowed you to pass an element, like Dojo’s byId function, you could do this using a union type:

function byId(element: string | Element): Element {
  if (typeof element === 'string') {
    return document.getElementById(element);
  }
  else {
    return element;
  }
}

TypeScript is intelligent enough to contextually type the element variable inside the if block to be of type string, and to be of type Element in the else block.

Intersection types

Intersection types require the value to meet the contract of all of the member types. For example:

interface Foo {
    name: string;
    count: number;
}

interface Bar {
    name: string;
    age: number;
}

export type FooBar = Foo & Bar;

One caveat is that you can accidentally make types that are unusable:

interface Foo {
    count: string;
}

interface Bar {
    count: number;
}

export type FooBar = Foo & Bar;
/* FooBar.name is now of type `string & Name` */

this typing

Between TypeScript 1.7 and 2.0, support was added to specific the type of this in a function, method, class or interface. In a function or method, this is a fake first parameter.

You may also use this parameters to declare how callbacks are invoked.

To avoid the behavior of typing for this to revert to the earlier behavior of TypeScript, you may use the --noImplicitThis compiler flag.

Mapped, Partial, Readonly, Record, and Pick Types (TS 2.1+)

A partial type is one where we take an existing type, but all of its properties are optional. This is common for APIs which accept a property bag as a parameter.

setState(this: StoreMixin, newState: Partial<State>): void {
	const { properties: { store, id } } = this;

	if (id || newState['id']) {
		store.patch(assign( { id }, newState))
			.then(() => id ? store.get(id) : store.fetch())
			.then((state: State) => {
				replaceState(this, state);
			});
	}
	else {
		throw new Error('Unable to set state without a specified `id`');
	}
}

With Mapped types, we can simplify the syntax to express this, by iterating over the original type using keyof, as a way to quickly create the new partial type. Mapped types are also useful for transforming types. For example, turning a group of synchronous properties into Promise instances.

readonly properties: Readonly<Partial<P>>;

Classes

The final major feature of TypeScript we have yet to discuss is the class-based inheritance syntax. The class system in TypeScript uses a single-inheritance model that should be familiar to any programmer that has ever worked with any class-based language. A basic class definition looks like this:

class Proxy {
  constructor(kwArgs: {}) {
    for (let key in kwArgs) {
      this[key] = kwArgs[key];
    }
  }

  get(key: string):any {
    return this[key];
  }

  set(key: {}): void;
  set(key: string, value: any): void;
  set(key: any, value?: any): void {
    // ...
  }
}

The special constructor method represents the JavaScript function used as the constructor when compiled back into JavaScript. This function can return a value to use as the instance if desired, just like JavaScript, but unlike all other methods of a class, constructor cannot have a defined return type; the return type of the constructor method is always the class itself.

Subclassing works like other class-based inheritance systems, using the extends keyword to create a subtype and the super identifier to refer to the superclass:

class Stateful extends Proxy {
  constructor(kwArgs: {}) {
    super(kwArgs);
  }

  get(key: string): any {
    let getter: string = '_' + key + 'Getter';
    return this[getter] ? this[getter]() : super.get(key);
  }
}

TypeScript classes may also define properties as being private, protected and/or static:

class Animal extends Stateful {
  protected _happy: boolean;

  pet(): void {
    this._happy = true;
  }
}

class Dog extends Animal {
  static isDogLike(object: any): boolean {
    return object.bark && object.pet;
  }

  private _loudBark: boolean;

  bark(): string {
    let noise = this._happy ? 'woof' : 'grr';
    if (this._loudBark) {
      noise = noise.toUpperCase();
    }

    return noise;
  }
}

Because property privacy is a compile-time constraint and not a runtime constraint, it’s a good idea to continue to follow JavaScript conventions for private properties and prefix with an underscore if your compiled TypeScript code might ever be consumed by someone writing pure JavaScript.

Property default values can also be specified within a class definition. The default value of a property can be any assignment expression, not just a static value, and will be executed every time a new instance is created:

class DomesticatedDog extends Dog {
  age: number = Math.random() * 20;
  collarType: string = 'leather';
  toys: Toy[] = [];
}

However, there are some caveats that come with defining default properties in this manner. Most notably, if you have defined a constructor function on a subclass, you must call super() before anything else within the constructor, which means you can’t perform operations before the superclass’s constructor runs, and your subclass’s default properties will not be set until after the superclass’s constructor runs. The alternative for this is to simply set the defaults in the constructor yourself:

class DomesticatedDog extends Dog {
  age: number;
  collarType: string;
  toys: Toy[];

  constructor(kwArgs: {}) {
    this.age = Math.random() * 20;
    this.collarType = 'leather';
    this.toys = [];
    super(kwArgs);
  }
}

Default properties are always set by TypeScript in the same manner as above, which means these two class definitions are equivalent from the perspective of how the default properties are set. As a result, you do not have to worry about objects or arrays being shared across instances as you would if they were specified on the prototype, which alleviates a common point of confusion for people using JavaScript “class-like” inheritance libraries that specify properties on the prototype.

Mixins and multiple inheritance

In TypeScript, interfaces can also extend classes, which can be useful when composing complex types, especially if you are used to writing mixins and using multiple inheritance:

interface Chimera extends Dog, Lion, Monsterish {}

class MyChimera implements Chimera {
  bark: () => string;
  roar: () => string;
  terrorize(): void {
    // ...
  }

  // ...
}
MyChimera.prototype.bark = Dog.prototype.bark;
MyChimera.prototype.roar = Lion.prototype.roar;

In this example, two classes (Dog, Lion) and an interface (Monsterish) have been combined into a new Chimera interface, and then a MyChimera class implements that interface, delegating back to the correct functions of the original classes. Note that the bark and roar methods are actually defined as properties rather than methods; this allows the interface to be “fully implemented” by the class despite the concrete implementation not actually existing within the class definition. This is one of the most advanced use cases for classes in TypeScript, but enables extremely robust and efficient code reuse when used properly.

TypeScript 2.2 made a number of changes to make mixins and compositional classes easier to work with. Rather than adding a new grammar to classes that might later conflict with the next version of ES, the TypeScript team achieved this result by removing some of the restrictions on classes. For example, it’s now possible to extend from a value that constructs an intersection type. The way signatures on intersection types get combined has also changed.

Enumerables

Finally, TypeScript adds a basic enum type that allows for efficient representation of sets of explicit values. For example, from the TypeScript specification, an enumeration of possible styles to apply to text might look like this:

enum Style {
  NONE = 0,
  BOLD = 1,
  ITALIC = 2,
  UNDERLINE = 4,
  EMPHASIS = Style.BOLD | Style.ITALIC,
  HYPERLINK = Style.BOLD | Style.UNDERLINE
}

In TypeScript, enumerator values are restricted by the compiler to be numbers. One benefit of enumerables being numbers is that you can use the bitwise OR operator, as shown above, to easily create values that are combinations of other enumerable values. As above, you can also explicitly define the value of a member of an enum using assignment. Enums that use bitwise operators should be specified to explicitly use 2n values for each item; enums are normally simple 0-indexed values.

Enumerable types in TypeScript are two-way maps, so you can determine the name of an enumerated value by looking it up in the enum object. Using the above example, Style[1] would evaluate to 'BOLD'.

The const enum type is the same as regular enumerable, except that the compiler replaces all references to enumerable values with literal values instead of generating code representing the enumerable structures at runtime.

Aliases

More robust type aliases can be used, which use the type keyword to provide the same sort of aliasing, but can also support aliasing of other primitive types:

import * as foo from './foo';
type Foo = foo.Foo;
type Bar = () => string;
type StringOrNumber = string | number;

function convert(value: StringOrNumber): string {
  return String(value);
}

Ambient declarations

In order to use existing JavaScript code with TypeScript, the compiler needs to be able to know what modules and variables come from outside TypeScript. To do this, TypeScript introduces the concept of an ambient declaration—a special declaration that provides type information about APIs that exist “ambiently” within the application’s execution environment.

Ambient declarations are created by prefixing any normal TypeScript module, var, let, const, function, class, or enum statement with the declare keyword, which indicates to the compiler that the statement is intended for ambient type hinting only. Since ambient declarations exist entirely for the benefit of the type system, they never include any implementation code, and they do not generate code on compilation.

For example, if you were to want to write code in TypeScript that used jQuery, the global jQuery function would need to be defined using an ambient declaration. In fact, many ambient declarations for various JavaScript libraries, including jQuery, can be found in the DefinitelyTyped project. The jquery.d.ts from DefinitelyTyped looks like this:

interface JQueryStatic {
  // ...

  (selector: string, context?: any): JQuery;
  (element: Element): JQuery;
  (object: {}): JQuery;
  (elementArray: Element[]): JQuery;
  (object: JQuery): JQuery;
  (func: Function): JQuery;
  (): JQuery;

  // ...
}

declare let jQuery: JQueryStatic;
declare let $: JQueryStatic;

Because ambient declarations don’t generate code, they are normally placed in files with an extension of .d.ts. Any file that ends in .d.ts instead of .ts will never generate a corresponding compiled module, so this file extension can also be useful for normal TypeScript modules that contain only interface definitions.

As touched on briefly when we discussed imports and exports, modules can also be defined as ambient declarations, which makes it possible to consume JavaScript code that is already properly modularized, like the Dojo Toolkit:

declare module 'dojo/_base/array' {
  let array: {
    every<T>(array: T[], callback: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
    filter<T>(array: T[], callback: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[];
    forEach<T>(array: T[], callback: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
    indexOf<T>(array: T[], value: T, fromIndex?: number, findLast?: boolean): number;
    lastIndexOf<T>(array: T[], value: T, fromIndex?: number): number;
    map<T>(array: T[], callback: (value: T, index: number, array: T[]) => T, thisArg?: any): T[];
    some<T>(array: T[], callback: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
  };
  export = array;
}

When you’re writing TypeScript code that needs access to ambient declarations, a special reference comment must be added to the top of the module that needs it:

/// <reference path="jquery" />

The path given in the reference comment can be either a standard module ID or a path to a file. If you use a filesystem path and get an error about TypeScript not being able to resolve the path, make sure that you have not accidentally typoed .ts as .js.

When writing modular code, reference comments should only ever be used to import ambient declarations; all other dependencies should be loaded using the import keyword. Happily, dependencies loaded using import that are never used, or that are only used for compile-time checks, will be intelligently excluded from the compiler output.

Loader plugins

If you’re an AMD user, you’ll probably be used to working with loader plugins (text! and the like). TypeScript doesn’t support importing these kinds of dynamic modules automatically, but it does have a mechanism for enabling their use.

To use an AMD loader plugin, you’ll need to use the <amd-dependency> directive. In TypeScript 1.4 and earlier, hypothetical usage of a text! plugin looks like this:

/// <amd-dependency path="text!foo.html" />
declare let require: (moduleId: string) => any;
const foo: string = require('text!foo.html');

TypeScript 1.5 added an name attribute for the <amd-dependency> directive that makes using AMD plugins easier:

/// <amd-dependency path="text!foo.html" name="foo" />
declare let foo: string;

TypeScript 2.0+ greatly simplified this through the addition of wildcard modules. To support module loader plugins within AMD or SystemJS, it’s necessary to be able to type the module, with the understanding that the name of the module is variable through the parameter that is passed to the module loader plugin. For example, this makes it possible to support the loading of HTML files, JSON resources, and other resources with more flexibility.

declare module "json!*" {
    let json: any;
    export default json;
}

import d from "json!a/b/bar.json";
// lookup:
//    json!a/b/bar.json
//    json!*

React and JSX support (TS 1.6+)

Numerous improvements have been made over the years to TypeScript to improve support for the JSX syntax that is popularized by React. More information may be found in the JSX Support documentation.

Control flow analysis (TS 1.8+)

Control flow analysis helps catch and prevent common errors. Examples of analysis features added include unreachable code, unused labels, implicit returns, case clause fall-throughs, narrowing and widening of types inline with the logic of your code, strict null checking, and String and number literal narrowing on strict equality, and better inference for literal types. Many of these changes can be overriden with compiler flags such as --allowUnreachableCode, --allowUnusedLabels, --noImplicitReturns, --noImplicitAny --noFallthroughCasesInSwitch, --strictNullChecks, etc.

Configuration

tsconfig.json is where most applications store information about various compiler flags and settings, as well as module path resolution information. An example tsconfig.json from the Dojo 2 project:

{
	"version": "2.1.5",
	"compilerOptions": {
		"declaration": false,
		"emitDecoratorMetadata": true,
		"experimentalDecorators": true,
		"module": "umd",
		"moduleResolution": "node",
		"noImplicitAny": true,
		"noImplicitThis": true,
		"outDir": "_build/",
		"removeComments": false,
		"sourceMap": true,
		"strictNullChecks": true,
		"target": "es5"
	},
	"include": [
		"./src/**/*.ts",
		"./tests/**/*.ts",
		"./typings/index.d.ts"
	]
}

Many projects also include a tslint.json to specify linter settings, a typings.json file to point to any type definition files from non-TypeScript dependencies, and a package.json file that is standard with most JavaScript packages.

Glob support was added for TypeScript 2.0, making it easy to include or exclude a group of files following patterns that leverage *, ? and **/.

Note that as of TypeScript 2.1, tsconfig.json can now inherit from other configuration files, reducing duplication across complex applications and libraries. This is done via the extends key which has a path as a value.

In conclusion

Our Advanced TypeScript post goes into more depth exploring how to use TypeScript’s class system, and explores some of TypeScript’s advanced features, such as symbols and decorators.

As TypeScript continues to evolve, it brings with it not just static typing, but also new features from the current and future ECMAScript specifications. This means you can safely start using TypeScript today without worrying that your code will need to be overhauled in a few months, or that you’ll need to switch to a new compiler to take advantage of the latest and greatest language features. Any breaking changes are documented in the TypeScript wiki.

If you want to get more detail on any of the features described in this guide, the TypeScript Language Specification is the authoritative resource on the language itself. Stack Overflow is also an excellent place to discuss TypeScript and ask questions, and the official TypeScript Handbook can also provide some additional insight above and beyond what this guide provides.

Learning more

We believe it’s more important than ever to learn the fundamentals of ES6+ and TypeScript. With the first substantial changes to the language in nearly 20 years, now is the time to learn how to efficiently leverage these changes to our primary language for creating web applications. We created an efficient workshop where we’ve distilled all of the valuable lessons we’ve learned over the past few years into a fast-paced ES6 & TypeScript for the Enterprise Developer workshop. This workshop is offered online or for your team at your location.

Finally, if you want more direct assistance, SitePen can provide you or your company with custom development or support services for TypeScript; just give us a holler to get started!

Comments

  • Pingback: TypeScript Cheat Sheet | Blog | SitePen()

  • dvisentin

    It’s really interesting that SitePen talks about TypeScript.
    How to mix TypeScript with Dojo? Maybe I’m wrong, but TS classes are incompatible with Dojo ones and viceversa, for example.

    PS:

    at this time there is no common support of Dojo (“d.ts” is missing in https://github.com/DefinitelyTyped/typescript-directory) but someone is trying to fix it (https://github.com/schungx/Dojo-TypeScript).

  • There is no incompatibility between TypeScript and Dojo classes; they both just become JavaScript constructor functions. schungx’s repository provides the class definitions that are necessary to use and extend existing Dojo APIs from TypeScript, and extending the existing Dojo classes can be done using the normal extends mechanism within TypeScript.

    Combining multiple classes together with mixins is not as easy within TypeScript as it is with dojo/_base/declare but it is still quite possible; simply follow the instructions in the part of the guide on mixins and multiple inheritance.

    Providing more native support within TypeScript for mixins or traits is something that we are interested in seeing in the future and it is a feature that the TS team are looking at adding for the next major version.

    Thanks for writing,

  • dojoVader

    Does that mean we should invest in TypeScript towards Dojo 2.0, in my opinion TypeScript doesn’t look that bad, but am assumning this is a going to be a major re-write of dojo/_base/declare.

  • It is a direction that we are seriously exploring.

  • schungx

    Or lobby Microsoft’s TypeScript developers to make class definitions more configurable such that the generated JavaScript can use the specific third-party-library’s own class declaration syntax. Microsoft may not care enough though, as they seem to be die-hards on jQuery and node.js. As long as jQuery and node.js works, it seems to be fine for them.

  • I’ve been talking with the TypeScript team and they are interested in ensuring that these sorts of extensions are possible in the future; you can see some of the progress at Mixins in TypeScript documentation and a discussion on a more robust solution for class combination. Perfect is the enemy of good, and I’d rather see a 1.0 release that makes these interactions possible even if it is a little messy, instead of never actually getting to 1.0. Of course, the ultimate goal should probably be to extend the built-in class system so that it can be made as powerful as any other solution.

  • Corey Alix

    I have been enjoying typescript and dojo for several months now. First using my own definitions and then schungx. I’m finding the transition from 0.9.1 to 0.9.5 painful in large part because only one developer has taken the initiative to define dojo.d.ts and related. If a team took on this burden we’d have a much easier time at it. I’m trying to resolve issues as I come across them but just don’t have the ability to get very far. Is this something sitepen is looking to take on in the near future?

  • Pingback: TypeScript Cheat Sheet()

  • At the moment, we’re probably more focused on exploring whether or not TypeScript is a good solution for future work such as Dojo 2. That said, if there’s demand, it’s certainly something we would consider working on, or at least finding a way to support a community effort to maintain TS interfaces for Dojo 1.9.x.

  • John Reilly

    Really nice run through guys – great job!

  • Corey Alix

    How about a definitive-guide-to-dojo-amd-typescript? I’ve struggled with making use of “class” to extend a sub-class of ContentPane but no luck. I can’t expose the interface. It’s possible?
    https://gist.github.com/ca0v/8441296

  • It is supposed to read 2^n but it seems that we don’t have an appropriate style for the <sup> tag!

  • Thanks! I’m glad it was helpful for you.

  • Have you tried, simply, class LayerStylePane extends ContentPane and putting addSymbol in that class?

  • Corey Alix

    Doh! That works!

  • dpaddock

    Colin, I’d love to hear your thoughts on this discussion:
    https://github.com/schungx/Dojo-TypeScript/issues/21

  • Corey Alix

    “Happily, dependencies loaded using import that are never used, or that are only used for compile-time checks, will be intelligently excluded from the compiler output.”

    This is not always true! When I build the project (VS2013) it injects the false dependency. Dependency is injected with a node build as well. Interestingly saving the file in VS does not generate the dependency!
    :
    Imagine how confusing this is…app doesn’t work; re-save a file without any changes; app works.

  • I’ve never seen any behaviour like what you are describing. I don’t use Visual Studio, only the command-line tsc, and always compile to AMD modules. In every case, an import statement whose value is referenced only by the type system is not emitted as a dependency in the output.

  • Pingback: 하이브리드앱 개발 on Visual Studio #1 | blog === undefined()

  • Pingback: Some TypeScript links of interest | Dino Zafirakos()

  • The last section on loader plugins doesn’t appear to work exactly as advertised in Dojo 1.10 for dojo/text and dojo/i18n. I had to pre-load those modules (with no plugin argument) and set their “dynamic” field to false. Otherwise, the loader assumes the content will be dynamically loaded and, therefore, doesn’t try to load from the cache.

  • The documentation has moved on a bit since this was written. See: http://www.typescriptlang.org/Handbook

  • Pingback: Testing TypeScript with Intern | Blog | SitePen()

  • Pingback: We're Contributing to Dojo 2! | Blog | SitePen()

  • Thorarin

    The bit about loader plugins seems incorrect. The name attribute doesn’t seem to do anything, at least for TypeScript 1.4. The code below works however; it’s an example with a regular import and two different loader plugins being used.

    ///
    ///
    declare function require(name: string): any;

    import other = require(“other”);
    var template: string = require(“text!template.html”);
    var resources = require(“i18n!resources”);

  • The name attribute for amd-dependency is new in TypeScript 1.5.

  • Thorarin

    That might have been worth mentioning, since it’s still in beta :)

  • I’ve updated the article to re-include some of the original information about loader plugins for TypeScript 1.4. (Limitation of the blog system prevent listing all the collaborators on this article, the update removing that content was not me :))

  • Pingback: I'm learning TypeScript - odoenet()

  • Pingback: | Recent TypeScript talks | Blog | SitePen()

  • Pingback: TypeScript, Elm and ArcGIS API for JavaScript - odoenet()

  • Pingback: | Advanced TypeScript concepts: Classes and types | Blog | SitePen()

  • Bitchiko

    Inside Generic types section : map method returns Array . How can you assign Array to Arrayish ?

  • I know it’s been a while since this was asked. But for anyone reading through these comments, it’s worth checking out http://github.com/dojo/typings

  • Note that dojo/compose is now in an initial beta state, http://github.com/dojo/compose

  • Corey Alix

    Would be nice to get this integrated with typings (https://github.com/typings/typings).

  • Paul Fernhout

    In general, as long as a given object has all of the properties that are required by the constraint on the variable being assigned to, they are considered compatible in TypeScript. So, the assignment in this case is accepted by TypeScript because an Array instance satisfies the minimum requirement of the Arrayish interface of supplying the minimum structure of a single method named “map”.

    One exception (which does not apply here) is a special case for object literal assignments that need to be more exact.

  • Bitchiko

    Thanks, Accepted answer.
    Addition, in case of object literal assignment, extra properties will not have a reference to them, if if were allowed.

  • Pingback: Using TypeScript with EsriJS 4 - odoenet()

  • andrew

    Thx for the great overview. One comment: Use of the fat arrow syntax to provide the type for a function (or method, etc) in Typescript is slightly misleading. As you know, arrow and non-arrow functions in ES6 are subtly but significantly different, e.g. in the use of ‘this’. Yet, if I understand correctly, when I characterize a function using ‘let fn: () => any;’ Typescript allows me to use either ‘fn = () => {…};’ or ‘fn = function() {…};’. I think this is acceptable enough, but it might be helpful to point out this discrepancy in this Typescript overview. Specifically, readers should know that the Typescript arrow syntax for typing functions is a more general abbreviation for functions than is true in ES6 in that it allows implementation using either arrow or non-arrow functions. Otherwise people new to Typescript might, e.g., think that the Typescript arrow typing allows only true arrow functions which is false.

  • Dave

    Does anybody know how to use ‘require’ syntax in the TypeScript context? i.e. Something like:

    require([“dojo/request/xhr”], dojo.hitch(this, function (xhr) {

    new xhr(url, { handleAs: handleAs, method: “GET” }).then(onPass), onFail

    }));

  • Generally speaking, you don’t. You use ES6 syntax. and then transpile to AMD or UMD with one of the options in your tsconfig.json. See https://github.com/dojo/typings/blob/master/examples/request/src/main.ts for an example with request using the provided typings.

    For what it’s worth, with AMD you should also be including dojo/_base/lang and then using lang.hitch.

  • Dave

    I appreciate your attempt at answering my question – truly I do. But man, you left me in the dust. ES6 syntax? transpile? tsconfig.json? I can see I need to do some reading. :)

  • Well, we do have an intensive workshop if that would speed things up, see https://www.sitepen.com/workshops/index.html?workshop=tses-online-07-2016 . Otherwise, we have a lot of blog posts and articles to cover an array of topics to help you get going. I’d suggest starting with the examples in the dojo/typings repo on GitHub!

  • Pingback: Intro to Dojo2 with ArcGIS API for JavaScript - odoenet()

  • Pingback: | Multi-Platform Distribution with TypeScript | Blog | SitePen()

  • Pingback: | Mixins and more in TypeScript 2.2 | Blog | SitePen()

  • rickoshay

    The ubiquitous use of semicolons reveals you need some education yourself about learning how to efficiently leverage changes in this now-core language. Completely useless semicolons are second only to Hungarian notation on the idiotic scale.