This article describes the features and functionality of TypeScript 3.1.
One of the most interesting languages for large-scale application development is Microsoft’s TypeScript. TypeScript is unique in that it is a superset of JavaScript (ES2015+), but with optional types, interfaces, generics, and more. Unlike other compile-to-JavaScript languages, TypeScript does not try to change JavaScript into a new language. Instead, the TypeScript team takes extra care to align the language’s extra features as closely as possible with what’s available in JavaScript and its upcoming features. Because of this, TypeScript developers are able to take advantage of the latest features in the JavaScript language in addition to a power type system to write better-organized code while taking advantage of advanced tooling that using a statically typed language provides.
The real power in TypeScript comes from its tooling. Types are a means to bring world-class tooling to the JavaScript language, which allows for better structured projects that are easier to maintain. This is especially important as JavaScript projects grow in size (both lines of code and developers on the project). Having fast, accurate completion, refactoring capabilities, and immediate feedback makes TypeScript the ideal language for large-scale JavaScript. TypeScript also makes it simple to get started! Since JavaScript is effectively TypeScript without type annotations, all or portions of an existing project can be converted immediately and then slowly take advantage of all that TypeScript has to offer over time.
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.
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.
Configuration
The TypeScript compiler is highly configurable, allowing the user to define where the source files are and how it should be transpiled to how strict the type checker should be and whether to allow JavaScript files. Each of the configuration options can be passed to the tsc
command, or a tsconfig.json file can be stored in a project to define how the compiler should run every time. The tsconfig.json stores information about various compiler flags and settings, as well as module path resolution information. A full list of compiler options is available. An example tsconfig.json from the Dojo project:
{
"version": "2.1.5",
"compilerOptions": {
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "umd",
"moduleResolution": "node",
"outDir": "_build/",
"removeComments": false,
"sourceMap": true,
"strict": true,
"target": "es5",
"lib": [
"es2015",
"dom"
]
},
"include": [
"./src/**/*.ts",
"./tests/**/*.ts",
"./typings/index.d.ts"
]
}
The main benefit to including a tsconfig.json in a project is to ensure that each run of the compiler is identical in its configuration to the previous run, and that all contributors to the project are running with the same flags. The compiler also has several flags that toggle how strict the compiler should be when it comes to type checking and whether to allow JavaScript files. This is one of the best parts of TypeScript as it allows for TypeScript to be added to an existing project without requiring that the entire project be converted to TypeScript and fully-typed first. Flags such as noImplicitAny
, when false
, will not emit a compiler warning about a variable that is not annotated with a type or where a type cannot be inferred. Over time, a project can enable this and other flags, allowing a team to work up, incrementally, towards fully-typed code. For new projects began in TypeScript, it is recommended that the strict
flag be enabled to receive the full benefit from TypeScript.
The ”lib"
property in the tsconfig can be used to specify what default libraries are available for the project. The values in here are based on the type of project and where the project needs to run. For example, a web project will likely need access to the DOM, so including ”dom”
will make sure that document
exists and contains methods such as querySelector
. If running in a ES2016 environment, or one with the appropriate polyfills, ”es2016”
can also be included. If your project specifically expects arrays to have an include
method, then ”es2016.array.include"
can be added. This allows for customization of how TypeScript can perceive the desired runtime environment and whether it should throw errors for using library code that may not actually exist at runtime.
As of TypeScript 2.7, a new reference lib directive exists to isolate the inclusion of a particular library to just the file using that directive. This is beneficial for isolating new/experimental features to a single file and not making them available to the wider project. Consider the following example:
/// <reference lib="es2016.array.include" />[ 'foo', 'bar', 'baz' ].includes('bar'); // true
The compiler will not throw an error about the use of Array.prototype.includes
in this module because it contains the lib directive. However, if another file in the project tried to use this method and the lib was not included in the tsconfig.json, the compiler would throw an error.
Many projects also include a tslint.json to specify linter settings 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 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.
Using let and const
ES2015 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';
By 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 .ts
, then .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, though efforts are underway in the named/mapped base URLs standards proposal.
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; // = 128new MyClass();
To import individual properties surround the property names with curly braces:
import { foo } from './myModule';import { bar, MyClass } from './myModule';foo();bar + 5; // = 128new MyClass();
You can specify a default export by adding the default
keyword immediately after export
:
export default class MyClass {}
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();
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.tsimport 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 also includes support for the arrow function from ES2015. 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 shorthand notations 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:
for..of
loop iteration- string templates
- tagged template strings
- exponentiation operator
- Generators and Iteration protocols (TS 1.6+ for ES2015 export, 2.3+ for ES3/ES5 export with
--downlevelIteration
) - async/await (TS 1.7+ for ES2015 export, 2.1+ for ES3/ES5 export)
for-await-of
loop iteration (TS 2.3+, ES3/ES5 with--downlevelIteration
)- Dynamic import expressions (TS 2.4+)
- Optional
catch
clause variables (TS 2.5+)
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), never
, and as of TypeScript 3.0, unknown
. 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. unknown
is the type safe counterpart of any
but where some type of type checking must be performed before the value can be used.
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);}}const num = <number> numberStringSwap('1234');const str = numberStringSwap(1234) as string;
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
is being explicitly asserted by prefixing the call expression with . 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 would result in a compiler error (“Cannot convert number to string”) since the two types are known to be incompatible. An alternative syntax for type-casting uses the as
keyword as seen on the assignment to the str
value above. Using as
is identical to using <>
. However, the <>
syntax is unavailable in .tsx files because of its ambiguity in TSX. Therefore, as
is the default and preferred syntax for type-casting.
When writing code in TypeScript, it is a good idea to explicitly add types to your variables and functions where the compiler cannot infer the type. When a variable is not annotated and the type cannot be inferred, it is given an implicit any
type. When compiling, setting "noImplicitAny": true
in the tsconfig.json compilerOptions section will 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 also has 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. let easing: "ease-in" | "ease-out" | "ease-in-out";
.
Local class, interface, enum, and type alias declarations may also appear inside function declarations. Scoping for local types is blocked, similar to variables declared with let
and const
.
Object types
In addition to the seven 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):
let point: { x: number; y: number; };point = { x: 0, y: 0 };// OK, properties matchpoint = { x: 'zero', y: 0 };// Error, `x` property type is wrongpoint = { x: 0 };// Error, missing required property `y`point = { x: 0, y: 0, z: 0 };// Error, object literal may only specify known propertiesconst 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 namedobject 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 matchpoint = { x: 0, y: 0 };// OK, properties match, optional property missingpoint = { 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.
Typescript 2.7 adds the ability to have constant-named properties on types. This means that interfaces can be defined using constant string, number, or Symbol literals.
const Foo = 'Foo';const Bar = 'Bar';const Baz = Symbol();interface MyInterface {[Foo]: number;[Bar]: string;[Baz]: boolean;}
object
type (TS 2.2+)
TypeScript 2.2 adds the the object
type which fixes a previous limitation in defining a type definition where something can be an Object
or a non-primitive type. This scenario was not possible to handle with previous versions of TypeScript because number
, string
, and boolean
could all be assigned to Object. 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 ];
TypeScript 3.0 adds better support for tuple types by allowing them to be used with rest and spread expressions, and allowing for optional elements.
function draw(...point: [ number, number, number? ]): void {const [ x, y, z ] = point;console.log('point', ...point);}draw(100, 200); // logs: point 100, 200draw(100, 200, 75); // logs: point 100, 200, 75draw(100, 200, 75, 25); // Error: Expected 2-3 arguments but got 4
In the above example, the draw
function can accept values for x
, y
, and optionally z
. Tuple types have a fixed length as of TypeScript 2.7 but also contain the ability to mark a value within the tuple as optional.
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 ES2015 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. 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.
Strict function types (TS 2.6+)
By default, TypeScript is lax on checking function type parameters. Consider the following example:
class Animal { breath() { } }class Dog extends Animal { bark() {} }class Cat extends Animal { meow() {} }let f1: (x: Animal) => void = (x: Animal) => x.breath();let f2: (x: Dog) => void = (x: Dog) => x.bark();let f3: (x: Cat) => void = (x: Cat) => x.meow();f1 = f2;const c = new Cat();f1(c); // Runtime error
If we were to set f1 = f2
this would result in a runtime error if we were to ever pass a Cat
to the function, even though Cat
is an Animal
, as it would try and call bark
, which cats cannot do. However, TypeScript will not throw an error on this by default. This is because function types in TypeScript are bivariant. To correct this, the TypeScript team added a new flag, --strictFunctionTypes
, which will throw an error on the f1 = f2
assignment.
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
. For example, an “array of strings” type would be Array
, and a “promise that resolves to a number” type would be Promise
. Generic types may require more than one specific type, like Converter
, 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
. 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.
In version 2.3, Typescript added the ability to declare a default type for generic types. This is very useful for functions where the return type is dependent upon a parameter, but you want the parameter to be optional. For example, if we wanted a function that created an Arrayish
based on the arguments passed but defaulted to string
when no arguments are passed, before 2.3 we would have had to write:
function createArrayish(): Arrayish<string>;function createArrayish<T>(...args: T[]): Arrayish<T>;function createArrayish(...args: any[]): Arrayish<any> {return args;}
Since 2.3, we can now write:
function createArrayish<T = string>(...args: T[]): Arrayish<T> {return args;}
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. The mechanism of narrowing types is referred to as Type Guards.
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 & number` */
this
typing
The context, or value of this
in a function, method, class, or interface can be defined. 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 Types (TS 2.1+)
Mapped Types allow for the creation of new types based on existing types by effectively mapping over an existing type and returning a new one. This allows for the transformation of each property on an existing type into a new one, reducing the type duplication. An example type could be one called Stringify
, which will take a type, T
, and transform each property on that type to one whose value is a string
.
type Stringify<T> = {[P in keyof T]: string;};interface Point { x: number; y: number; }type StringPoint = Partial<Point>;const pointA: StringPoint = { x: '4', Y: '3' }; // valid
Mapped, Partial, Readonly, Record, and Pick Types (TS 2.1+)
Partial, Readonly, Record, and Pick are mapped types that are so common that TypeScript provides these for you. 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.
type ToPomise<T> = { [K in typeof T]: Promise<T[K> };
TypeScript 2.8 adds the ability to add or remove readonly
or ?
modifiers to mapped properties. This is done using +
and -
to indicate whether the modifier should be added or removed.
type MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] };type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] };interface Point { readonly x: number; y: number; }const pointA: ReadonlyPartial<Point> = { x: 4 };pointA.y = 3; // Error: readonlyconst pointB: MutableRequired<Point> = { x: 4, y: 3 };pointB.x = 2; // valid
TypeScript 3.1 also introduces the ability to map over tuple types and return a new tuple type. Consider the following example where a tuple type for Point
s is defined. Suppose that in some cases the points will actually be Promises that resolve to Point
objects. TypeScript allows for the creation of the latter type from the former.
type ToPromise<T> = { [K in typeof T]: Promise<T[K]> };type Point = [ number, number ];type PromisePoint = ToPromise<Point>;const point: PromisePoint = [ Promise.resolve(2), Promise.resolve(3) ]; // valid}
Conditional types (TS 2.8+)
Conditional types allow for a type to be set dynamically based on a provided condition. All conditional types follow the same format: T extends U ? X : Y
. This should look familiar to you as it is the syntax for a ternary statement. What this statement means is that if T
is assignable to U
, then set the type to X
. Otherwise, set the type to Y
. This may seem simple at first but it is a very powerful way to ensure that the proper types are provided for your code.
Consider the following example where we would like to define types for a function that accepts either a number or a string.
declare function addOrConcat(x: number | string): number | string;
The types here are fine but they do not truly convey the meaning or intent of the code. Presumably, if the argument is a number
then the return type will also be number
, and likewise with string
. To correct this, we can use function overloading.
declare function addOrConcat(x: string): string;declare function addOrConcat(x: number): number;declare function addOrConcat(x: number | string): number | string;
However this is a little verbose and can be tedious to change in the future. Enter conditional types! Using the ternary syntax of conditional types above, we can simplify our function signature to the following:
declare function addOrConcat<T extends number | string>(x: T): T extends number ? number : string;
This function signature is using generics, stating that T
will either be a number
or a string
. This will be the value for the argument. Then, conditional types are used to determine the return type, stating that the function will return a number
if it was passed one, otherwise a string
.
TypeScript also ships with some common conditional types that serve the purpose of handling common situations of using conditional types and also showing you their true power.
Exclude
– exclude fromT
anything that is assignable toU
Extract
– extract fromT
anything that is assignable toU
NonNullable
– excludenull
andundefined
ReturnType
– get the return type of a functionInstanceType
– get the instance type of a constructor function
type ExcludeExample = Exclude<string | number | Point, string | Point>; // numbertype ExtractExample = Extract<string | number | Point, string | Point>; // string | Pointtype NonNullableExample = NonNullable<string | number | null | undefined | void>; // string | numbertype ReturnTypeExample = ReturnType<() => boolean>; // booleanclass Renderer {}type InstanceOfExample = InstanceType<typeof Renderer>; // Renderer
Type Guards
Type guards allow for narrowing of types within a conditional block. This is essential when working with types that could be unions of two or more types, or where the type is not known until runtime. To do this in a way that is also compatible with the JavaScript code that will be run at runtime, the type system ties into the typeof
, instanceof
, and in
(as of TS 2.7) operators. Inside of a conditional block using one of these checks, it is guaranteed that the value checked is of that type and, and methods that would exist on that type can be used safely.
Using the typeof
type guard
The typeof
type guard will key in on the result of using the typeof
operator in JavaScript. If the execution were to enter into the conditional block, then TypeScript can correctly assume that the value is of the type being checked. However, if execution were to continue in the else
block, TypeScript can assume that the value is not that type, and in the case of a union type, it is the alternative.
function lower(x: string | string[]) {if (typeof x === 'string') {// x is guaranteed to be a string, so we can use toLowerCasereturn x.toLowerCase();} else {// x is definitely an array of strings, so we can use reducereturn x.reduce((val: string, next: string) => {return val += `, ${next.toLowerCase()}`;}, '');}}
Using the instanceof
type guards
The instanced
type guard will use the result of JavaScript’s instanceof
operator to narrow types within a conditional statement. Much like the typeof
usage above, if the result of this type check is true, then TypeScript can correctly infer that the variable in question is of that type within the conditional block.
function clearElement(element: string | HTMLElement) {if (element instanceof HTMLElement) {// element is guaranteed to be an HTMLElement in here// so we can access its innerHTML propertyelement.innerHTML = '';} else {// element is a string in here so we can pass that to querySelectorconst el = document.querySelector(elmeent);el && el.innerHTML = '';}}
Using the in
type guard
This type guard narrows the type within a conditional by checking if a property exists on the variable. If the result if true
, the variable type will be narrowed to match the type that contains the value checked on.
interface Point {x: number;y: number;}interface Point3d extends Point {z: number;}function plot(point: Point) {if ('z' in point) {// point is a Point3D// ...} else {// point is a Point// ...}}
Classes
One 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. It is important to note that JavaScript classes are primarily a method of syntactical sugar over the existing prototype-based inheritance system and do not introduce a new object-oriented inheritance model to JavaScript.
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 {// ...}}
For the most part, classes in Typescript are similar to classes in ES2015. 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);}}
Where TypeScript differs from ES2015 classes currently is in the use of non-method fields, however, there is a Stage 3 proposal to add this to JavaScript. 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 run-time 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
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}if (value & Style.BOLD) {// handles BOLD, EMPHASIS, and HYPERLINK}
When enumerator values are integers, you can use, as shown above, the bitwise OR operator to create bitmask values and use the bitwise AND operator to check if a value is set in the bitmask. 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.
Prior to version 2.4, enumerator values were restricted by the compiler to numbers. In version 2.4 and beyond, enumerator values can be strings:
enum Color {Red = "RED",Green = "GREEN",Blue = "BLUE"}
Numeric 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 Style
above example, Style[1]
would evaluate to 'BOLD'
. String-initialized enums cannot be reverse mapped.
The const enum
type is the same as a 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;type PromiseOrValue<T> = T | Promise<T>;function convert(value: StringOrNumber): string {return String(value);}function when<T>(value: PromiseOrValue<T>): Promise<T> {if (value instanceof Promise) {return value;}return Promise.resolve(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:
// ...(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 import
ing 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');
There exists a name
attribute for the 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
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
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.
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 ES2015+ 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. SitePen is happy to provide you or your company with help developing your next application; just give us a holler to get started!
Other posts in the series
Follow SitePen on Twitter.