Typescript en
Typescript en
1
typescript
Table of contents
En - Nightly Builds 6
Declaration files - By Example 8
Declaration files - Consumption 13
Declaration files - Deep Dive 14
Declaration files - Do's and Don'ts 18
Declaration files - Introduction 22
Declaration files - Library Structures 24
Declaration files - Publishing 30
Declaration files - Templates 34
Templates - Global modifying module.d.ts 35
Templates - Global plugin.d.ts 37
Templates - Global.d.ts 43
Templates - Module class.d.ts 46
Templates - Module function.d.ts 48
Templates - Module plugin.d.ts 50
Templates - Module.d.ts 52
Get started - TS for Functional Programmers 58
Get started - TS for JS Programmers 68
Get started - TS for OOPers 74
Get started - TS for the New Programmer 79
Handbook v1 - Basic Types 83
Handbook v1 - Classes 91
Handbook v1 - Functions 102
Handbook v1 - Generics 112
Handbook v1 - Interfaces 119
Handbook v1 - Literal Types 132
Handbook v1 - Unions and Intersections 134
Handbook v2 - Basics 140
Handbook v2 - Classes 149
Handbook v2 - Everyday Types 174
Handbook v2 - Modules 188
Handbook v2 - More on Functions 196
Handbook v2 - Narrowing 212
Handbook v2 - Object Types 226
2
TYPESCRIPT Docs - English
3
typescript
4
TYPESCRIPT Docs - English
5
typescript
A nightly build from the TypeScript's main branch is published by midnight PST to npm. Here is how you
can get it and use it with your tools.
Using npm
npm install -g typescript@next
Sublime Text
Update the Settings - User file with the following:
More information is available at the TypeScript Plugin for Sublime Text installation documentation.
Note: Most changes do not require you to install a new version of the VS TypeScript plugin.
The nightly build currently does not include the full plugin setup, but we are working on publishing an in-
staller on a nightly basis as well.
Also see our wiki page on using a custom language service file.
For VS 2015:
6
Nightly Builds
For VS 2013:
Go to TOC
7
typescript
The purpose of this guide is to teach you how to write a high-quality definition file. This guide is structured
by showing documentation for some API, along with sample usage of that API, and explaining how to write
the corresponding declaration.
The global variable myLib has a function makeGreeting for creating greetings, and a property numb
erOfGreetings indicating the number of greetings made so far.
Code
Declaration
Overloaded Functions
Documentation
The getWidget function accepts a number and returns a Widget, or accepts a string and returns a Widget
array.
Code
Declaration
8
Declaration Reference
When specifying a greeting, you must pass a GreetingSettings object. This object has the following
properties:
Code
greet({
greeting: "hello world",
duration: 4000
});
Declaration
interface GreetingSettings {
greeting: string;
duration?: number;
color?: string;
}
Anywhere a greeting is expected, you can provide a string , a function returning a string , or a Gr
eeter instance.
Code
function getGreeting() {
return "howdy";
}
class MyGreeter extends Greeter {}
9
typescript
greet("hello");
greet(getGreeting);
greet(new MyGreeter());
Declaration
Organizing Types
Documentation
The greeter object can log to a file or display an alert. You can provide LogOptions to .log(...)
and alert options to .alert(...)
Code
Declaration
10
Declaration Reference
Classes
Documentation
You can create a greeter by instantiating the Greeter object, or create a customized greeter by ex-
tending from it.
Code
Declaration
Use declare class to describe a class or class-like object. Classes can have properties and methods as
well as a constructor.
greeting: string;
showGreeting(): void;
}
Global Variables
Documentation
Code
Declaration
Use declare var to declare variables. If the variable is read-only, you can use declare const . You can
also use declare let if the variable is block-scoped.
11
typescript
Global Functions
Documentation
You can call the function greet with a string to show a greeting to the user.
Code
greet("hello, world");
Declaration
Go to TOC
12
Consumption
Downloading
Getting type declarations requires no tools apart from npm.
As an example, getting the declarations for a library like lodash takes nothing more than the following
command
It is worth noting that if the npm package already includes its declaration file as described in Publishing,
downloading the corresponding @types package is not needed.
Consuming
From there you’ll be able to use lodash in your TypeScript code with no fuss. This works for both modules
and global code.
For example, once you’ve npm install -ed your type declarations, you can use imports and write
or if you’re not using modules, you can just use the global variable _ .
Searching
For the most part, type declaration packages should always have the same name as the package name on
npm , but prefixed with @types/ , but if you need, you can check out this Type Search to find the package
for your favorite library.
Note: if the declaration file you are searching for is not present, you can always contribute one back
and help out the next developer looking for it. Please see the DefinitelyTyped contribution guidelines
page for details.
Go to TOC
13
typescript
By reading this guide, you'll have the tools to write complex declaration files that expose a friendly API sur-
face. This guide focuses on module (or UMD) libraries because the options here are more varied.
Key Concepts
You can fully understand how to make any shape of declaration by understanding some key concepts of how
TypeScript works.
Types
If you're reading this guide, you probably already roughly know what a type in TypeScript is. To be more ex-
plicit, though, a type is introduced with:
Values
As with types, you probably already understand what a value is. Values are runtime names that we can ref-
erence in expressions. For example let x = 5; creates a value called x .
Namespaces
Types can exist in namespaces. For example, if we have the declaration let x: A.B.C , we say that the
type C comes from the A.B namespace.
This distinction is subtle and important -- here, A.B is not necessarily a type or a value.
14
Deep Dive
This may seem confusing, but it's actually very convenient as long as we don't excessively overload things.
Let's look at some useful aspects of this combining behavior.
Built-in Combinations
Astute readers will notice that, for example, class appeared in both the type and value lists. The declara-
tion class C { } creates two things: a type C which refers to the instance shape of the class, and a val-
ue C which refers to the constructor function of the class. Enum declarations behave similarly.
User Combinations
Let's say we wrote a module file foo.d.ts :
This works well enough, but we might imagine that SomeType and SomeVar were very closely related such
that you'd like them to have the same name. We can use combining to present these two different objects
(the value and the type) under the same name Bar :
This presents a very good opportunity for destructuring in the consuming code:
Again, we've used Bar as both a type and a value here. Note that we didn't have to declare the Bar value
as being of the Bar type -- they're independent.
15
typescript
Advanced Combinations
Some kinds of declarations can be combined across multiple declarations. For example, class C { } and
interface C { } can co-exist and both contribute properties to the C types.
This is legal as long as it does not create a conflict. A general rule of thumb is that values always conflict
with other values of the same name unless they are declared as namespace s, types will conflict if they are
declared with a type alias declaration ( type s = string ), and namespaces never conflict.
interface Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK
class Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK
Note that we cannot add to type aliases ( type s = string; ) using an interface.
class C {}
// ... elsewhere ...
namespace C {
export let x: number;
}
let y = C.x; // OK
16
Deep Dive
Note that in this example, we added a value to the static side of C (its constructor function). This is be-
cause we added a value, and the container for all values is another value (types are contained by name-
spaces, and namespaces are contained by other namespaces).
class C {}
// ... elsewhere ...
namespace C {
export interface D {}
}
let y: C.D; // OK
In this example, there wasn't a namespace C until we wrote the namespace declaration for it. The mean-
ing C as a namespace doesn't conflict with the value or type meanings of C created by the class.
Finally, we could perform many different merges using namespace declarations. This isn't a particularly re-
alistic example, but shows all sorts of interesting behavior:
namespace X {
export interface Y {}
export class Z {}
}
In this example, the first block creates the following name meanings:
Go to TOC
17
typescript
General Types
Number , String , Boolean , Symbol and Object
❌ Don't ever use the types Number , String , Boolean , Symbol , or Object These types refer to non-
primitive boxed objects that are almost never used appropriately in JavaScript code.
/* WRONG */
function reverse(s: String): String;
/* OK */
function reverse(s: string): string;
Instead of Object , use the non-primitive object type (added in TypeScript 2.2).
Generics
❌ Don't ever have a generic type which doesn't use its type parameter. See more details in TypeScript FAQ
page.
any
❌ Don't use any as a type unless you are in the process of migrating a JavaScript project to TypeScript.
The compiler effectively treats any as "please turn off type checking for this thing". It is similar to putting
an @ts-ignore comment around every usage of the variable. This can be very helpful when you are first
migrating a JavaScript project to TypeScript as you can set the type for stuff you haven't migrated yet as
any , but in a full TypeScript project you are disabling type checking for any parts of your program that use
it.
In cases where you don't know what type you want to accept, or when you want to accept anything because
you will be blindly passing it through without interacting with it, you can use unknown .
Callback Types
Return Types of Callbacks
❌ Don't use the return type any for callbacks whose value will be ignored:
/* WRONG */
function fn(x: () => any) {
x();
}
✅ Do use the return type void for callbacks whose value will be ignored:
18
Do's and Don'ts
/* OK */
function fn(x: () => void) {
x();
}
❔ Why: Using void is safer because it prevents you from accidentally using the return value of x in an
unchecked way:
/* WRONG */
interface Fetcher {
getObject(done: (data: unknown, elapsedTime?: number) => void): void;
}
This has a very specific meaning: the done callback might be invoked with 1 argument or might be invoked
with 2 arguments. The author probably intended to say that the callback might not care about the
elapsedTime parameter, but there's no need to make the parameter optional to accomplish this -- it's al-
ways legal to provide a callback that accepts fewer arguments.
/* OK */
interface Fetcher {
getObject(done: (data: unknown, elapsedTime: number) => void): void;
}
/* WRONG */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(
action: (done: DoneFn) => void,
timeout?: number
): void;
/* OK */
declare function beforeAll(
action: (done: DoneFn) => void,
timeout?: number
): void;
19
typescript
❔ Why: It's always legal for a callback to disregard a parameter, so there's no need for the shorter over-
load. Providing a shorter callback first allows incorrectly-typed functions to be passed in because they match
the first overload.
Function Overloads
Ordering
❌ Don't put more general overloads before more specific overloads:
/* WRONG */
declare function fn(x: unknown): unknown;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;
✅ Do sort overloads by putting the more general signatures after more specific signatures:
/* OK */
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: unknown): unknown;
❔ Why: TypeScript chooses the first matching overload when resolving function calls. When an earlier
overload is "more general" than a later one, the later one is effectively hidden and cannot be called.
/* WRONG */
interface Example {
diff(one: string): number;
diff(one: string, two: string): number;
diff(one: string, two: string, three: boolean): number;
}
/* OK */
interface Example {
diff(one: string, two?: string, three?: boolean): number;
}
Note that this collapsing should only occur when all overloads have the same return type.
20
Do's and Don'ts
TypeScript resolves signature compatibility by seeing if any signature of the target can be invoked with the
arguments of the source, and extraneous arguments are allowed. This code, for example, exposes a bug
only when the signature is correctly written using optional parameters:
The second reason is when a consumer uses the "strict null checking" feature of TypeScript. Because un-
specified parameters appear as undefined in JavaScript, it's usually fine to pass an explicit undefined to
a function with optional arguments. This code, for example, should be OK under strict nulls:
var x: Example;
// When written with overloads, incorrectly an error because of passing
'undefined' to 'string'
// When written with optionals, correctly OK
x.diff("something", true ? undefined : "hour");
/* WRONG */
interface Moment {
utcOffset(): number;
utcOffset(b: number): Moment;
utcOffset(b: string): Moment;
}
/* OK */
interface Moment {
utcOffset(): number;
utcOffset(b: number | string): Moment;
}
Note that we didn't make b optional here because the return types of the signatures differ.
❔ Why: This is important for people who are "passing through" a value to your function:
Go to TOC
21
typescript
The Declaration Files section is designed to teach you how to write a high-quality TypeScript Declaration
File. We need to assume basic familiarity with the TypeScript language in order to get started.
If you haven't already, you should read the TypeScript Handbook to familiarize yourself with basic concepts,
especially types and modules.
The most common case for learning how .d.ts files work is that you're typing an npm package with no
types. In that case, you can jump straight to Modules .d.ts.
The Declaration Files section is broken down into the following sections.
Declaration Reference
We are often faced with writing a declaration file when we only have examples of the underlying library to
guide us. The Declaration Reference section shows many common API patterns and how to write declara-
tions for each of them. This guide is aimed at the TypeScript novice who may not yet be familiar with every
language construct in TypeScript.
Library Structures
The Library Structures guide helps you understand common library formats and how to write a proper dec-
laration file for each format. If you're editing an existing file, you probably don't need to read this section.
Authors of new declaration files are strongly encouraged to read this section to properly understand how the
format of the library influences the writing of the declaration file.
In the Template section you'll find a number of declaration files that serve as a useful starting point when
writing a new file. If you already know what your structure is, see the d.ts Template section in the sidebar.
Deep Dive
For seasoned authors interested in the underlying mechanics of how declaration files work, the Deep Dive
section explains many advanced concepts in declaration writing, and shows how to leverage these concepts
to create cleaner and more intuitive declaration files.
Publish to npm
The Publishing section explains how to publish your declaration files to an npm package, and shows how to
manage your dependent packages.
22
Introduction
Go to TOC
23
typescript
Broadly speaking, the way you structure your declaration file depends on how the library is consumed.
There are many ways of offering a library for consumption in JavaScript, and you'll need to write your decla-
ration file to match it. This guide covers how to identify common library patterns, and how to write declara-
tion files which correspond to that pattern.
Each type of major library structuring pattern has a corresponding file in the Templates section. You can
start with these templates to help you get going faster.
Identifying the structure of a library is the first step in writing its declaration file. We'll give hints on how to
identify structure both based on its usage and its code. Depending on the library's documentation and orga-
nization, one might be easier than the other. We recommend using whichever is more comfortable to you.
For example, can you only get it through npm or only from a CDN?
Does it add a global object? Does it use require or import / export statements?
ECMAScript 2015 (also known as ES2015, ECMAScript 6, and ES6), CommonJS, and RequireJS have similar
notions of importing a module. In JavaScript CommonJS (Node.js), for example, you would write
var fs = require("fs");
You'll typically see modular libraries include one of these lines in their documentation:
24
Library Structures
or
});
As with global modules, you might see these examples in the documentation of a UMD module, so be sure
to check the code or documentation.
You should first read module.d.ts for an overview on the way they all work.
Then use the template module-function.d.ts if your module can be called like a function:
const x = require("foo");
// Note: calling 'x' as a function
const y = x(42);
Use the template module-class.d.ts if your module can be constructed using new :
const x = require("bar");
// Note: using 'new' operator on the imported variable
const y = new x("hello");
If you have a module which when imported, makes changes to other modules use template module-
plugin.d.ts :
25
typescript
Global Libraries
A global library is one that can be accessed from the global scope (i.e. without using any form of import ).
Many libraries simply expose one or more global variables for use. For example, if you were using jQuery,
the $ variable can be used by simply referring to it:
$(() => {
console.log("hello!");
});
You'll usually see guidance in the documentation of a global library of how to use the library in an HTML
script tag:
<script src="http://a.great.cdn.for/someLib.js"></script>
Today, most popular globally-accessible libraries are actually written as UMD libraries (see below). UMD li-
brary documentation is hard to distinguish from global library documentation. Before writing a global decla-
ration file, make sure the library isn't actually UMD.
function createGreeting(s) {
return "Hello, " + s;
}
or like this:
// Web
window.createGreeting = function (s) {
return "Hello, " + s;
};
// Node
global.createGreeting = function (s) {
return "Hello, " + s;
};
26
Library Structures
UMD
A UMD module is one that can either be used as module (through an import), or as a global (when run in an
environment without a module loader). Many popular libraries, such as Moment.js, are written this way. For
example, in Node.js or using RequireJS, you would write:
console.log(moment.format());
If you see tests for typeof define , typeof window , or typeof module in the code of a library, especial-
ly at the top of the file, it's almost always a UMD library.
Documentation for UMD libraries will also often demonstrate a "Using in Node.js" example showing re‐
quire , and a "Using in the browser" example showing using a <script> tag to load the script.
27
typescript
Template
Use the module-plugin.d.ts template.
Consuming Dependencies
There are several kinds of dependencies your library might have. This section shows how to import them
into the declaration file.
Dependencies on Modules
If your library depends on a module, use an import statement:
28
Library Structures
Footnotes
Preventing Name Conflicts
Note that it's possible to define many types in the global scope when writing a global declaration file. We
strongly discourage this as it leads to possible unresolvable name conflicts when many declaration files are
in a project.
A simple rule to follow is to only declare types namespaced by whatever global variable the library defines.
For example, if the library defines the global value 'cats', you should write
But not
// at top-level
interface CatsKittySettings {}
This guidance also ensures that the library can be transitioned to UMD without breaking declaration file
users.
In ES6-compliant module loaders, the top-level object (here imported as exp ) can only have properties;
the top-level module object can never be callable.
The most common solution here is to define a default export for a callable/constructable object; module
loaders commonly detect this situation automatically and replace the top-level object with the default ex-
port. TypeScript can handle this for you, if you have "esModuleInterop": true in your tsconfig.json.
Go to TOC
29
typescript
Now that you have authored a declaration file following the steps of this guide, it is time to publish it to
npm. There are two main ways you can publish your declaration files to npm:
If your types are generated by your source code, publish the types with your source code. Both TypeScript
and JavaScript projects can generate types via declaration .
Otherwise, we recommend submitting the types to DefinitelyTyped, which will publish them to the @types
organization on npm.
{
"name": "awesome",
"author": "Vandelay Industries",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts"
}
Note that the "typings" field is synonymous with types , and could be used as well.
Also note that if your main declaration file is named index.d.ts and lives at the root of the package (next
to index.js ) you do not need to mark the types property, though it is advisable to do so.
Dependencies
All dependencies are managed by npm. Make sure all the declaration packages you depend on are marked
appropriately in the "dependencies" section in your package.json . For example, imagine we authored a
package that used Browserify and TypeScript.
{
"name": "browserify-typescript-extension",
"author": "Vandelay Industries",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts",
"dependencies": {
"browserify": "latest",
"@types/browserify": "latest",
"typescript": "next"
}
}
30
Publishing
Here, our package depends on the browserify and typescript packages. browserify does not bundle
its declaration files with its npm packages, so we needed to depend on @types/browserify for its declara-
tions. typescript , on the other hand, packages its declaration files, so there was no need for any addi-
tional dependencies.
Our package exposes declarations from each of those, so any user of our browserify-typescript-exten‐
sion package needs to have these dependencies as well. For that reason, we used "dependencies" and
not "devDependencies" , because otherwise our consumers would have needed to manually install those
packages. If we had just written a command line application and not expected our package to be used as a
library, we might have used devDependencies .
Red flags
/// <reference path="..." />
Don't use /// <reference path="..." /> in your declaration files.
Make sure to revisit the Consuming dependencies section for more information.
{
"name": "package-name",
"version": "1.0.0",
"types": "./index.d.ts",
"typesVersions": {
31
typescript
This package.json tells TypeScript to first check the current version of TypeScript. If it's 3.1 or later,
TypeScript figures out the path you've imported relative to the package, and reads from the package's
ts3.1 folder.
That's what that { "*": ["ts3.1/*"] } means - if you're familiar with path mapping, it works exactly like
that.
In the above example, if we're importing from "package-name" , TypeScript will try to resolve from
[...]/node_modules/package-name/ts3.1/index.d.ts (and other relevant paths) when running in
TypeScript 3.1. If we import from package-name/foo , we'll try to look for [...]/node_modules/package-
name/ts3.1/foo.d.ts and [...]/node_modules/package-name/ts3.1/foo/index.d.ts .
What if we're not running in TypeScript 3.1 in this example? Well, if none of the fields in typesVersions
get matched, TypeScript falls back to the types field, so here TypeScript 3.0 and earlier will be redirected
to [...]/node_modules/package-name/index.d.ts .
File redirects
When you want to only change the resolution for a single file at a time, you can tell TypeScript the file to re-
solve differently by passing in the exact filenames:
{
"name": "package-name",
"version": "1.0.0",
"types": "./index.d.ts",
"typesVersions": {
"<4.0": { "index.d.ts": ["index.v3.d.ts"] }
}
}
On TypeScript 4.0 and above, an import for "package-name" would resolve to ./index.d.ts and for 3.9
and below "./index.v3.d.ts .
Matching behavior
The way that TypeScript decides on whether a version of the compiler & language matches is by using
Node's semver ranges.
Multiple fields
typesVersions can support multiple fields where each field name is specified by the range to match on.
{
"name": "package-name",
"version": "1.0",
"types": "./index.d.ts",
"typesVersions": {
32
Publishing
Since ranges have the potential to overlap, determining which redirect applies is order-specific. That means
in the above example, even though both the >=3.2 and the >=3.1 matchers support TypeScript 3.2 and
above, reversing the order could have different behavior, so the above sample would not be equivalent to
the following.
{
"name": "package-name",
"version": "1.0",
"types": "./index.d.ts",
"typesVersions": {
// NOTE: this doesn't work!
">=3.1": { "*": ["ts3.1/*"] },
">=3.2": { "*": ["ts3.2/*"] }
}
}
Publish to @types
Packages under the @types organization are published automatically from DefinitelyTyped using the types-
publisher tool. To get your declarations published as an @types package, please submit a pull request to
DefinitelyTyped. You can find more details in the contribution guidelines page.
Go to TOC
33
typescript
global-modifying-module.d.ts
global-plugin.d.ts
global.d.ts
module-class.d.ts
module-function.d.ts
module-plugin.d.ts
module.d.ts
Go to TOC
34
Global: Modifying Module
Global-modifying Modules
A global-modifying module alters existing values in the global scope when they are imported. For example,
there might exist a library which adds new members to String.prototype when imported. This pattern is
somewhat dangerous due to the possibility of runtime conflicts, but we can still write a declaration file for it.
Here is an example
/*~ This is the global-modifying module template file. You should rename it to
index.d.ts
*~ and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
35
typescript
/*~ For example, declaring a method on the module (in addition to its global side
effects) */
export function doSomething(): void;
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
export {};
Go to TOC
36
Global: Plugin
UMD
A UMD module is one that can either be used as module (through an import), or as a global (when run in an
environment without a module loader). Many popular libraries, such as Moment.js, are written this way. For
example, in Node.js or using RequireJS, you would write:
console.log(moment.format());
If you see tests for typeof define , typeof window , or typeof module in the code of a library, especial-
ly at the top of the file, it's almost always a UMD library.
Documentation for UMD libraries will also often demonstrate a "Using in Node.js" example showing re‐
quire , and a "Using in the browser" example showing using a <script> tag to load the script.
Template
There are three templates available for modules, module.d.ts , module-class.d.ts and module-
function.d.ts .
var x = require("foo");
// Note: calling 'x' as a function
var y = x(42);
Be sure to read the footnote "The Impact of ES6 on Module Call Signatures"
37
typescript
var x = require("bar");
// Note: using 'new' operator on the imported variable
var y = new x("hello");
For the purposes of writing a declaration file, you'll write the same code whether the module being changed
is a plain module or UMD module.
Template
Use the module-plugin.d.ts template.
Global Plugin
A global plugin is global code that changes the shape of some global. As with global-modifying modules,
these raise the possibility of runtime conflict.
Template
Use the global-plugin.d.ts template.
38
Global: Plugin
Global-modifying Modules
A global-modifying module alters existing values in the global scope when they are imported. For example,
there might exist a library which adds new members to String.prototype when imported. This pattern is
somewhat dangerous due to the possibility of runtime conflicts, but we can still write a declaration file for it.
Template
Use the global-modifying-module.d.ts template.
Consuming Dependencies
There are several kinds of dependencies your library might have. This section shows how to import them
into the declaration file.
Dependencies on Modules
If your library depends on a module, use an import statement:
39
typescript
Footnotes
Preventing Name Conflicts
Note that it's possible to define many types in the global scope when writing a global declaration file. We
strongly discourage this as it leads to possible unresolvable name conflicts when many declaration files are
in a project.
A simple rule to follow is to only declare types namespaced by whatever global variable the library defines.
For example, if the library defines the global value 'cats', you should write
But not
// at top-level
interface CatsKittySettings {}
This guidance also ensures that the library can be transitioned to UMD without breaking declaration file
users.
40
Global: Plugin
In ES6 module loaders, the top-level object (here imported as exp ) can only have properties; the top-level
module object is never callable. The most common solution here is to define a default export for a
callable/constructable object; some module loader shims will automatically detect this situation and replace
the top-level object with the default export.
myLib
+---- index.js
+---- foo.js
+---- bar
+---- index.js
+---- baz.js
var a = require("myLib");
var b = require("myLib/foo");
var c = require("myLib/bar");
var d = require("myLib/bar/baz");
@types/myLib
+---- index.d.ts
+---- foo.d.ts
+---- bar
+---- index.d.ts
+---- baz.d.ts
/*~ Write a declaration for the original type and add new members.
*~ For example, this adds a 'toBinaryString' method with overloads to
*~ the built-in number type.
*/
interface Number {
toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string;
41
typescript
toBinaryString(
callback: MyLibrary.BinaryFormatCallback,
opts?: MyLibrary.BinaryFormatOptions
): string;
}
/*~ If you need to declare several types, place them inside a namespace
*~ to avoid adding too many things to the global namespace.
*/
declare namespace MyLibrary {
type BinaryFormatCallback = (n: number) => string;
interface BinaryFormatOptions {
prefix?: string;
padding: number;
}
}
Go to TOC
42
Global .d.ts
Global Libraries
A global library is one that can be accessed from the global scope (i.e. without using any form of import ).
Many libraries simply expose one or more global variables for use. For example, if you were using jQuery,
the $ variable can be used by simply referring to it:
$(() => {
console.log("hello!");
});
You'll usually see guidance in the documentation of a global library of how to use the library in an HTML
script tag:
<script src="http://a.great.cdn.for/someLib.js"></script>
Today, most popular globally-accessible libraries are actually written as UMD libraries (see below). UMD li-
brary documentation is hard to distinguish from global library documentation. Before writing a global decla-
ration file, make sure the library isn't actually UMD.
function createGreeting(s) {
return "Hello, " + s;
}
or like this:
43
typescript
/*~ If you want the name of this library to be a valid type name,
*~ you can do so here.
*~
*~ For example, this allows us to write 'var x: myLib';
*~ Be sure this actually makes sense! If it doesn't, just
*~ delete this declaration and add types inside the namespace below.
*/
interface myLib {
name: string;
length: number;
extras?: string[];
}
//~ There's some class we can create via 'let c = new myLib.Cat(42)'
//~ Or reference e.g. 'function f(c: myLib.Cat) { ... }
class Cat {
constructor(n: number);
44
Global .d.ts
Go to TOC
45
typescript
For example, when you want to work with JavaScript code which looks like:
/*~ If this module is a UMD module that exposes a global variable 'myClassLib'
when
*~ loaded outside a module loader environment, declare that global here.
*~ Otherwise, delete this declaration.
*/
export as namespace "super-greeter";
greet: void;
/*~ If you want to expose types from your module as well, you can
*~ place them in this block.
*~
*~ Note that if you decide to include this namespace, the module can be
*~ incorrectly imported as a namespace object, unless
46
Module: Class
Go to TOC
47
typescript
For example, when you want to work with JavaScript code which looks like:
greeter(2);
greeter("Hello world");
/*~ If this module is a UMD module that exposes a global variable 'myFuncLib' when
*~ loaded outside a module loader environment, declare that global here.
*~ Otherwise, delete this declaration.
*/
export as namespace myFuncLib;
/*~ This example shows how to have multiple overloads for your function */
declare function Greeter(name: string): Greeter.NamedReturnType;
declare function Greeter(length: number): Greeter.LengthReturnType;
/*~ If you want to expose types from your module as well, you can
*~ place them in this block. Often you will want to describe the
*~ shape of the return type of the function; that type should
*~ be declared in here, as this example shows.
*~
*~ Note that if you decide to include this namespace, the module can be
*~ incorrectly imported as a namespace object, unless
*~ --esModuleInterop is turned on:
*~ import * as x from '[~THE MODULE~]'; // WRONG! DO NOT DO THIS!
*/
declare namespace Greeter {
48
Module: Function
/*~ If the module also has properties, declare them here. For example,
*~ this declaration says that this code is legal:
*~ import f = require('super-greeter');
*~ console.log(f.defaultName);
*/
export const defaultName: string;
export let defaultLength: number;
}
Go to TOC
49
typescript
For example, when you want to work with JavaScript code which extends another library.
/*~ This example shows how to have multiple overloads for your function */
export interface GreeterFunction {
(name: string): void
(time: number): void
}
/*~ This is the module plugin template file. You should rename it to index.d.ts
*~ and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ On this line, import the module which this module adds to */
import { greeter } from "super-greeter";
/*~ Here, declare the same module as the one you imported above
*~ then we expand the existing declaration of the greeter function
*/
export module "super-greeter" {
export interface GreeterFunction {
/** Greets even better! */
hyperGreet(): void;
}
}
50
Module: Plugin
Go to TOC
51
typescript
function getArrayLength(arr) {
return arr.length;
}
module.exports = {
getArrayLength,
maxInterval,
};
The TypeScript playground can show you the .d.ts equivalent for JavaScript code. You can try it yourself
here.
The .d.ts syntax intentionally looks like ES Modules syntax. ES Modules was ratified by TC39 in 2019,
while it has been available via transpilers for a long time, however if you have a JavaScript codebase using
ES Modules:
Default Exports
In CommonJS you can export any value as the default export, for example here is a regular expression
module:
52
Modules .d.ts
Or a number:
module.exports = 3.142;
One style of exporting in CommonJS is to export a function. Because a function is also an object, then extra
fields can be added and are included in the export.
function getArrayLength(arr) {
return arr.length;
}
getArrayLength.maxInterval = 12;
module.exports = getArrayLength;
Note that using export default in your .d.ts files requires esModuleInterop: true to work. If you can't
have esModuleInterop: true in your project, such as when you're submitting a PR to Definitely Typed,
you'll have to use the export= syntax instead. This older syntax is harder to use but works everywhere.
Here's how the above example would have to be written using export= :
export = getArrayLength;
See Module: Functions for details of how that works, and the Modules reference page.
Covering all of these cases requires the JavaScript code to actually support all of these patterns. To support
many of these patterns, a CommonJS module would need to look something like:
53
typescript
class FastifyInstance {}
function fastify() {
return new FastifyInstance();
}
fastify.FastifyInstance = FastifyInstance;
Types in Modules
You may want to provide a type for JavaScript code which does not exist
function getArrayMetadata(arr) {
return {
length: getArrayLength(arr),
firstObject: arr[0],
};
}
module.exports = {
getArrayMetadata,
};
This example is a good case for using generics to provide richer type information:
Now the type of the array propagates into the ArrayMetadata type.
The types which are exported can then be re-used by consumers of the modules using either import or
import type in TypeScript code or JSDoc imports.
54
Modules .d.ts
For example, you may have complex enough types to describe that you choose to namespace them inside
your .d.ts :
// This namespace is merged with the API class and allows for consumers, and this
file
// to have types which are nested away in their own sections.
declare namespace API {
export interface InfoRequest {
id: string;
}
To understand how namespaces work in .d.ts files read the .d.ts deep dive.
Reference Example
To give you an idea of how all these pieces can come together, here is a reference .d.ts to start with
when making a new module
/*~ This is the module template file. You should rename it to index.d.ts
*~ and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ If this module is a UMD module that exposes a global variable 'myLib' when
*~ loaded outside a module loader environment, declare that global here.
*~ Otherwise, delete this declaration.
*/
55
typescript
/*~ You can declare types that are available via importing the module */
export interface SomeType {
name: string;
length: number;
extras?: string[];
}
/*~ You can declare properties of the module using const, let, or var */
export const myField: number;
myLib
+---- index.js
+---- foo.js
+---- bar
+---- index.js
+---- baz.js
var a = require("myLib");
var b = require("myLib/foo");
var c = require("myLib/bar");
var d = require("myLib/bar/baz");
@types/myLib
+---- index.d.ts
+---- foo.d.ts
+---- bar
+---- index.d.ts
+---- baz.d.ts
56
Modules .d.ts
4. When you're happy, clone DefinitelyTyped/DefinitelyTyped and follow the instructions in the
README.
Otherwise
Go to TOC
57
typescript
TypeScript began its life as an attempt to bring traditional object-oriented types to JavaScript so that the
programmers at Microsoft could bring traditional object-oriented programs to the web. As it has developed,
TypeScript's type system has evolved to model code written by native JavaScripters. The resulting system is
powerful, interesting and messy.
This introduction is designed for working Haskell or ML programmers who want to learn TypeScript. It de-
scribes how the type system of TypeScript differs from Haskell's type system. It also describes unique fea-
tures of TypeScript's type system that arise from its modelling of JavaScript code.
This introduction does not cover object-oriented programming. In practice, object-oriented programs in
TypeScript are similar to those in other popular languages with OO features.
Prerequisites
In this introduction, I assume you know the following:
If you need to learn the good parts of JavaScript, read JavaScript: The Good Parts. You may be able to skip
the book if you know how to write programs in a call-by-value lexically scoped language with lots of muta-
bility and not much else. R4RS Scheme is a good example.
The C++ Programming Language is a good place to learn about C-style type syntax. Unlike C++, TypeScript
uses postfix types, like so: x: string instead of string x .
Type Explanation
58
TypeScript for Functional Programmers
number
string
bigint
boolean
symbol
null
undefined
object
Type Explanation
Notes:
1. Function syntax includes parameter names. This is pretty hard to get used to!
// or more precisely:
2. Object literal type syntax closely mirrors object literal value syntax:
3. [T, T] is a subtype of T[] . This is different than Haskell, where tuples are not related to lists.
Boxed types
JavaScript has boxed equivalents of primitive types that contain the methods that programmers associate
with those types. TypeScript reflects this with, for example, the difference between the primitive type num‐
ber and the boxed type Number . The boxed types are rarely needed, since their methods return primitives.
59
typescript
(1).toExponential();
// equivalent to
Number.prototype.toExponential.call(1);
Note that calling a method on a numeric literal requires it to be in parentheses to aid the parser.
Gradual typing
TypeScript uses the type any whenever it can't tell what the type of an expression should be. Compared to
Dynamic , calling any a type is an overstatement. It just turns off the type checker wherever it appears.
For example, you can push any value into an any[] without marking the value in any way:
any is contagious, too — if you initialize a variable with an expression of type any , the variable has type
any too.
To get an error when TypeScript produces an any , use "noImplicitAny": true , or "strict": true in
tsconfig.json .
Structural typing
Structural typing is a familiar concept to most functional programmers, although Haskell and most MLs are
not structurally typed. Its basic form is pretty simple:
// @strict: false
let o = { x: "hi", extra: 1 }; // ok
let o2: { x: string } = o; // ok
Here, the object literal { x: "hi", extra: 1 } has a matching literal type { x: string, extra: num‐
ber } . That type is assignable to { x: string } since it has all the required properties and those proper-
ties have assignable types. The extra property doesn't prevent assignment, it just makes it a subtype of {
x: string } .
Named types just give a name to a type; for assignability purposes there's no difference between the type
alias One and the interface type Two below. They both have a property p: string . (Type aliases behave
differently from interfaces with respect to recursive definitions and type parameters, however.)
60
TypeScript for Functional Programmers
// @errors: 2322
type One = { p: string };
interface Two {
p: string;
}
class Three {
p = "Hello";
}
Unions
In TypeScript, union types are untagged. In other words, they are not discriminated unions like data in
Haskell. However, you can often discriminate types in a union using built-in tags or other properties.
function start(
arg: string | string[] | (() => string) | { s: string }
): string {
// this is super common in JavaScript
if (typeof arg === "string") {
return commonCase(arg);
} else if (Array.isArray(arg)) {
return arg.map(commonCase).join(",");
} else if (typeof arg === "function") {
return commonCase(arg());
} else {
return commonCase(arg.s);
}
string , Array and Function have built-in type predicates, conveniently leaving the object type for the
else branch. It is possible, however, to generate unions that are difficult to differentiate at runtime. For
new code, it's best to build only discriminated unions.
Type Predicate
61
typescript
Type Predicate
array Array.isArray(a)
Note that functions and arrays are objects at runtime, but have their own predicates.
Intersections
In addition to unions, TypeScript also has intersections:
Combined has two properties, a and b , just as if they had been written as one object literal type.
Intersection and union are recursive in case of conflicts, so Conflicting.a: number & string .
Unit types
Unit types are subtypes of primitive types that contain exactly one primitive value. For example, the string
"foo" has the type "foo" . Since JavaScript has no built-in enums, it is common to use a set of well-
known strings instead. Unions of string literal types allow TypeScript to type this pattern:
When needed, the compiler widens — converts to a supertype — the unit type to the primitive type, such as
"foo" to string . This happens when using mutability, which can hamper some uses of mutable
variables:
// @errors: 2345
declare function pad(s: string, n: number, direction: "left" | "right"): string;
// ---cut---
let s = "right";
pad("hi", 10, s); // error: 'string' is not assignable to '"left" | "right"'
"right": "right"
s: string because "right" widens to string on assignment to a mutable variable.
string is not assignable to "left" | "right"
You can work around this with a type annotation for s , but that in turn prevents assignments to s of vari-
ables that are not of type "left" | "right" .
62
TypeScript for Functional Programmers
But it also infers types in a few other places that you may not expect if you've worked with other C-syntax
languages:
Here, n: number in this example also, despite the fact that T and U have not been inferred before the
call. In fact, after [1,2,3] has been used to infer T=number , the return type of n => n.toString() is
used to infer U=string , causing sns to have the type string[] .
Note that inference will work in any order, but intellisense will only work left-to-right, so TypeScript prefers
to declare map with the array first:
Contextual typing also works recursively through object literals, and on unit types that would otherwise be
inferred as string or number . And it can infer return types from context:
1. Declaration initializers are contextually typed by the declaration's type: { inference: string } .
2. The return type of a call uses the contextual type for inferences, so the compiler infers that T={ infer‐
ence: string } .
3. Arrow functions use the contextual type to type their parameters, so the compiler gives o: {
inference: string } .
And it does so while you are typing, so that after typing o. , you get completions for the property infer‐
ence , along with any other properties you'd have in a real program. Altogether, this feature can make
TypeScript's inference look a bit like a unifying type inference engine, but it is not.
Type aliases
Type aliases are mere aliases, just like type in Haskell. The compiler will attempt to use the alias name
wherever it was used in the source code, but does not always succeed.
63
typescript
An FString is just like a normal string, except that the compiler thinks it has a property named __com‐
pileTimeOnly that doesn't actually exist. This means that FString can still be assigned to string , but
not the other way round.
Discriminated Unions
The closest equivalent to data is a union of types with discriminant properties, normally called discriminat-
ed unions in TypeScript:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
Unlike Haskell, the tag, or discriminant, is just a property in each object type. Each variant has an identical
property with a different unit type. This is still a normal union type; the leading | is an optional part of the
union type syntax. You can discriminate the members of the union using normal JavaScript code:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
Note that the return type of area is inferred to be number because TypeScript knows the function is total.
If some variant is not covered, the return type of area will be number | undefined instead.
Also, unlike Haskell, common properties show up in any union, so you can usefully discriminate multiple
members of the union:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
// ---cut---
function height(s: Shape) {
if (s.kind === "circle") {
return 2 * s.radius;
64
TypeScript for Functional Programmers
} else {
// s.kind: "square" | "triangle"
return s.x;
}
}
Type Parameters
Like most C-descended languages, TypeScript requires declaration of type parameters:
There is no case requirement, but type parameters are conventionally single uppercase letters. Type para-
meters can also be constrained to a type, which behaves a bit like type class constraints:
TypeScript can usually infer type arguments from a call based on the type of the arguments, so type argu-
ments are usually not needed.
Because TypeScript is structural, it doesn't need type parameters as much as nominal systems. Specifically,
they are not needed to make a function polymorphic. Type parameters should only be used to propagate
type information, such as constraining parameters to be the same type:
In the first length , T is not necessary; notice that it's only referenced once, so it's not being used to con-
strain the type of the return value or other parameters.
Higher-kinded types
TypeScript does not have higher kinded types, so the following is not legal:
Point-free programming
Point-free programming — heavy use of currying and function composition — is possible in JavaScript, but
can be verbose. In TypeScript, type inference often fails for point-free programs, so you'll end up specifying
type parameters instead of value parameters. The result is so verbose that it's usually better to avoid point-
free programming.
65
typescript
Module system
JavaScript's modern module syntax is a bit like Haskell's, except that any file with import or export is
implicitly a module:
You can also import commonjs modules — modules written using node.js' module system:
import f = require("single-function-package");
export { f };
function f() {
return g();
}
function g() {} // g is not exported
The latter style is more common but both are allowed, even in the same file.
interface Rx {
readonly x: number;
}
let rx: Rx = { x: 1 };
rx.x = 12; // error
It also ships with a mapped type Readonly<T> that makes all properties readonly :
interface X {
x: number;
}
let rx: Readonly<X> = { x: 1 };
rx.x = 12; // error
66
TypeScript for Functional Programmers
And it has a specific ReadonlyArray<T> type that removes side-affecting methods and prevents writing to
indices of the array, as well as special syntax for this type:
You can also use a const-assertion, which operates on arrays and object literals:
However, none of these options are the default, so they are not consistently used in TypeScript code.
Next Steps
This doc is a high level overview of the syntax and types you would use in everyday code. From here you
should:
Go to TOC
67
typescript
TypeScript stands in an unusual relationship to JavaScript. TypeScript offers all of JavaScript's features, and
an additional layer on top of these: TypeScript's type system.
For example, JavaScript provides language primitives like string and number , but it doesn't check that
you've consistently assigned these. TypeScript does.
This means that your existing working JavaScript code is also TypeScript code. The main benefit of
TypeScript is that it can highlight unexpected behavior in your code, lowering the chance of bugs.
This tutorial provides a brief overview of TypeScript, focusing on its type system.
Types by Inference
TypeScript knows the JavaScript language and will generate types for you in many cases. For example in
creating a variable and assigning it to a particular value, TypeScript will use the value as its type.
By understanding how JavaScript works, TypeScript can build a type-system that accepts JavaScript code
but has types. This offers a type-system without needing to add extra characters to make types explicit in
your code. That's how TypeScript knows that helloWorld is a string in the above example.
You may have written JavaScript in Visual Studio Code, and had editor auto-completion. Visual Studio Code
uses TypeScript under the hood to make it easier to work with JavaScript.
Defining Types
You can use a wide variety of design patterns in JavaScript. However, some design patterns make it difficult
for types to be inferred automatically (for example, patterns that use dynamic programming). To cover
these cases, TypeScript supports an extension of the JavaScript language, which offers places for you to tell
TypeScript what the types should be.
For example, to create an object with an inferred type which includes name: string and id: number ,
you can write:
const user = {
name: "Hayes",
id: 0,
};
You can explicitly describe this object's shape using an interface declaration:
interface User {
name: string;
id: number;
}
68
TypeScript for JavaScript Programmers
You can then declare that a JavaScript object conforms to the shape of your new interface by using syn-
tax like : TypeName after a variable declaration:
interface User {
name: string;
id: number;
}
// ---cut---
const user: User = {
name: "Hayes",
id: 0,
};
If you provide an object that doesn't match the interface you have provided, TypeScript will warn you:
// @errors: 2322
interface User {
name: string;
id: number;
}
Since JavaScript supports classes and object-oriented programming, so does TypeScript. You can use an in-
terface declaration with classes:
interface User {
name: string;
id: number;
}
class UserAccount {
name: string;
id: number;
You can use interfaces to annotate parameters and return values to functions:
// @noErrors
interface User {
name: string;
id: number;
}
// ---cut---
function getAdminUser(): User {
//...
}
69
typescript
There is already a small set of primitive types available in JavaScript: boolean , bigint , null , number ,
string , symbol , and undefined , which you can use in an interface. TypeScript extends this list with a
few more, such as any (allow anything), unknown (ensure someone using this type declares what the type
is), never (it's not possible that this type could happen), and void (a function which returns undefined
or has no return value).
You'll see that there are two syntaxes for building types: Interfaces and Types. You should prefer inter‐
face . Use type when you need specific features.
Composing Types
With TypeScript, you can create complex types by combining simple ones. There are two popular ways to do
so: with unions, and with generics.
Unions
With a union, you can declare that a type could be one of many types. For example, you can describe a
boolean type as being either true or false :
Note: If you hover over MyBool above, you'll see that it is classed as boolean . That's a property of the
Structural Type System. More on this below.
A popular use-case for union types is to describe the set of string or number literals that a value is al-
lowed to be:
Unions provide a way to handle different types too. For example, you may have a function that takes an
array or a string :
Type Predicate
70
TypeScript for JavaScript Programmers
Type Predicate
array Array.isArray(a)
For example, you can make a function return different values depending on whether it is passed a string or
an array:
Generics
Generics provide variables to types. A common example is an array. An array without generics could contain
anything. An array with generics can describe the values that the array contains.
// @errors: 2345
interface Backpack<Type> {
add: (obj: Type) => void;
get: () => Type;
}
// Since the backpack variable is a string, you can't pass a number to the add
function.
backpack.add(23);
In a structural type system, if two objects have the same shape, they are considered to be of the same
type.
71
typescript
interface Point {
x: number;
y: number;
}
The point variable is never declared to be a Point type. However, TypeScript compares the shape of
point to the shape of Point in the type-check. They have the same shape, so the code passes.
// @errors: 2345
interface Point {
x: number;
y: number;
}
// @errors: 2345
interface Point {
x: number;
y: number;
}
72
TypeScript for JavaScript Programmers
If the object or class has all the required properties, TypeScript will say they match, regardless of the imple-
mentation details.
Next Steps
This was a brief overview of the syntax and tools used in everyday TypeScript. From here, you can:
Go to TOC
73
typescript
TypeScript is a popular choice for programmers accustomed to other languages with static typing, such as
C# and Java.
TypeScript's type system offers many of the same benefits, such as better code completion, earlier detec-
tion of errors, and clearer communication between parts of your program. While TypeScript provides many
familiar features for these developers, it's worth stepping back to see how JavaScript (and therefore
TypeScript) differ from traditional OOP languages. Understanding these differences will help you write better
JavaScript code, and avoid common pitfalls that programmers who go straight from C#/Java to TypeScript
may fall in to.
Co-learning JavaScript
If you're familiar with JavaScript already but are primarily a Java or C# programmer, this introductory page
can help explain some of the common misconceptions and pitfalls you might be susceptible to. Some of the
ways that TypeScript models types are quite different from Java or C#, and it's important to keep these in
mind when learning TypeScript.
If you're a Java or C# programmer that is new to JavaScript in general, we recommend learning a little bit
of JavaScript without types first to understand JavaScript's runtime behaviors. Because TypeScript doesn't
change how your code runs, you'll still have to learn how JavaScript works in order to write code that actu-
ally does something!
It's important to remember that TypeScript uses the same runtime as JavaScript, so any resources about
how to accomplish specific runtime behavior (converting a string to a number, displaying an alert, writing a
file to disk, etc.) will always apply equally well to TypeScript programs. Don't limit yourself to TypeScript-
specific resources!
Static Classes
Additionally, certain constructs from C# and Java such as singletons and static classes are unnecessary in
TypeScript.
74
TypeScript for Java/C# Programmers
OOP in TypeScript
That said, you can still use classes if you like! Some problems are well-suited to being solved by a tradition-
al OOP hierarchy, and TypeScript's support for JavaScript classes will make these models even more power-
ful. TypeScript supports many common patterns such as implementing interfaces, inheritance, and static
methods.
Rethinking Types
TypeScript's understanding of a type is actually quite different from C# or Java's. Let's explore some
differences.
These aspects describe a reified, nominal type system. The types we wrote in the code are present at run-
time, and the types are related via their declarations, not their structures.
Types as Sets
In C# or Java, it's meaningful to think of a one-to-one correspondence between runtime types and their
compile-time declarations.
In TypeScript, it's better to think of a type as a set of values that share something in common. Because
types are just sets, a particular value can belong to many sets at the same time.
Once you start thinking of types as sets, certain operations become very natural. For example, in C#, it's
awkward to pass around a value that is either a string or int , because there isn't a single type that rep-
resents this sort of value.
In TypeScript, this becomes very natural once you realize that every type is just a set. How do you describe
a value that either belongs in the string set or the number set? It simply belongs to the union of those
sets: string | number .
TypeScript provides a number of mechanisms to work with types in a set-theoretic way, and you'll find them
more intuitive if you think of types as sets.
75
typescript
interface Pointlike {
x: number;
y: number;
}
interface Named {
name: string;
}
const obj = {
x: 0,
y: 0,
name: "Origin",
};
logPoint(obj);
logName(obj);
TypeScript's type system is structural, not nominal: We can use obj as a Pointlike because it has x
and y properties that are both numbers. The relationships between types are determined by the properties
they contain, not whether they were declared with some particular relationship.
TypeScript's type system is also not reified: There's nothing at runtime that will tell us that obj is
Pointlike . In fact, the Pointlike type is not present in any form at runtime.
Going back to the idea of types as sets, we can think of obj as being a member of both the Pointlike
set of values and the Named set of values.
Empty Types
class Empty {}
76
TypeScript for Java/C# Programmers
TypeScript determines if the call to fn here is valid by seeing if the provided argument is a valid Empty . It
does so by examining the structure of { k: 10 } and class Empty { } . We can see that { k: 10 }
has all of the properties that Empty does, because Empty has no properties. Therefore, this is a valid call!
This may seem surprising, but it's ultimately a very similar relationship to one enforced in nominal OOP lan-
guages. A subclass cannot remove a property of its base class, because doing so would destroy the natural
subtype relationship between the derived class and its base. Structural type systems simply identify this re-
lationship implicitly by describing subtypes in terms of having properties of compatible types.
Identical Types
class Car {
drive() {
// hit the gas
}
}
class Golfer {
drive() {
// hit the ball far
}
}
// No error?
let w: Car = new Golfer();
Again, this isn't an error because the structures of these classes are the same. While this may seem like a
potential source of confusion, in practice, identical classes that shouldn't be related are not common.
We'll learn more about how classes relate to each other in the Classes chapter.
Reflection
OOP programmers are accustomed to being able to query the type of any value, even a generic one:
// C#
static void LogType<T>() {
Console.WriteLine(typeof(T).Name);
}
Because TypeScript's type system is fully erased, information about e.g. the instantiation of a generic type
parameter is not available at runtime.
JavaScript does have some limited primitives like typeof and instanceof , but remember that these op-
erators are still working on the values as they exist in the type-erased output code. For example, typeof
(new Car()) will be "object" , not Car or "Car" .
77
typescript
Next Steps
This was a brief overview of the syntax and tools used in everyday TypeScript. From here, you can:
Go to TOC
78
TypeScript for the New Programmer
Congratulations on choosing TypeScript as one of your first languages — you're already making good
decisions!
You've probably already heard that TypeScript is a "flavor" or "variant" of JavaScript. The relationship be-
tween TypeScript (TS) and JavaScript (JS) is rather unique among modern programming languages, so
learning more about this relationship will help you understand how TypeScript adds to JavaScript.
Web browser developers responded to this increased JS usage by optimizing their execution engines (dy-
namic compilation) and extending what could be done with it (adding APIs), which in turn made web devel-
opers use it even more. On modern websites, your browser is frequently running applications that span
hundreds of thousands of lines of code. This is long and gradual growth of "the web", starting as a simple
network of static pages, and evolving into a platform for rich applications of all kinds.
More than this, JS has become popular enough to be used outside the context of browsers, such as imple-
menting JS servers using node.js. The "run anywhere" nature of JS makes it an attractive choice for cross-
platform development. There are many developers these days that use only JavaScript to program their en-
tire stack!
To summarize, we have a language that was designed for quick uses, and then grew to a full-fledged tool to
write applications with millions of lines. Every language has its own quirks — oddities and surprises, and
JavaScript's humble beginning makes it have many of these. Some examples:
if ("" == 0) {
// It is! But why??
}
if (1 < x < 3) {
// True for *any* value of x!
}
79
typescript
Most programming languages would throw an error when these sorts of errors occur, some would do so dur-
ing compilation — before any code is running. When writing small programs, such quirks are annoying but
manageable; when writing applications with hundreds or thousands of lines of code, these constant surpris-
es are a serious problem.
TypeScript checks a program for errors before execution, and does so based on the kinds of values, it's a
static type checker. For example, the last example above has an error because of the type of obj . Here's
the error TypeScript found:
// @errors: 2551
const obj = { width: 10, height: 15 };
const area = obj.width * obj.heigth;
Syntax
TypeScript is a language that is a superset of JavaScript: JS syntax is therefore legal TS. Syntax refers to
the way we write text to form a program. For example, this code has a syntax error because it's missing a
):
// @errors: 1005
let a = (4
TypeScript doesn't consider any JavaScript code to be an error because of its syntax. This means you can
take any working JavaScript code and put it in a TypeScript file without worrying about exactly how it is
written.
Types
However, TypeScript is a typed superset, meaning that it adds rules about how different kinds of values can
be used. The earlier error about obj.heigth was not a syntax error: it is an error of using some kind of
value (a type) in an incorrect way.
As another example, this is JavaScript code that you can run in your browser, and it will log a value:
console.log(4 / []);
This syntactically-legal program logs Infinity . TypeScript, though, considers division of number by an ar-
ray to be a nonsensical operation, and will issue an error:
80
TypeScript for the New Programmer
// @errors: 2363
console.log(4 / []);
It's possible you really did intend to divide a number by an array, perhaps just to see what happens, but
most of the time, though, this is a programming mistake. TypeScript's type checker is designed to allow
correct programs through while still catching as many common errors as possible. (Later, we'll learn about
settings you can use to configure how strictly TypeScript checks your code.)
If you move some code from a JavaScript file to a TypeScript file, you might see type errors depending on
how the code is written. These may be legitimate problems with the code, or TypeScript being overly con-
servative. Throughout this guide we'll demonstrate how to add various TypeScript syntax to eliminate such
errors.
Runtime Behavior
TypeScript is also a programming language that preserves the runtime behavior of JavaScript. For example,
dividing by zero in JavaScript produces Infinity instead of throwing a runtime exception. As a principle,
TypeScript never changes the runtime behavior of JavaScript code.
This means that if you move code from JavaScript to TypeScript, it is guaranteed to run the same way,
even if TypeScript thinks that the code has type errors.
Keeping the same runtime behavior as JavaScript is a foundational promise of TypeScript because it means
you can easily transition between the two languages without worrying about subtle differences that might
make your program stop working.
Erased Types
Roughly speaking, once TypeScript's compiler is done with checking your code, it erases the types to pro-
duce the resulting "compiled" code. This means that once your code is compiled, the resulting plain JS code
has no type information.
This also means that TypeScript never changes the behavior of your program based on the types it inferred.
The bottom line is that while you might see type errors during compilation, the type system itself has no
bearing on how your program works when it runs.
Finally, TypeScript doesn't provide any additional runtime libraries. Your programs will use the same stan-
dard library (or external libraries) as JavaScript programs, so there's no additional TypeScript-specific
framework to learn.
The answer is that you can't learn TypeScript without learning JavaScript! TypeScript shares syntax and run-
time behavior with JavaScript, so anything you learn about JavaScript is helping you learn TypeScript at the
same time.
81
typescript
There are many, many resources available for programmers to learn JavaScript; you should not ignore
these resources if you're writing TypeScript. For example, there are about 20 times more StackOverflow
questions tagged javascript than typescript , but all of the javascript questions also apply to
TypeScript.
If you find yourself searching for something like "how to sort a list in TypeScript", remember: TypeScript is
JavaScript's runtime with a compile-time type checker. The way you sort a list in TypeScript is the
same way you do so in JavaScript. If you find a resource that uses TypeScript directly, that's great too, but
don't limit yourself to thinking you need TypeScript-specific answers for everyday questions about how to
accomplish runtime tasks.
Next Steps
This was a brief overview of the syntax and tools used in everyday TypeScript. From here, you can:
Go to TOC
82
Basic Types
For programs to be useful, we need to be able to work with some of the simplest units of data: numbers,
strings, structures, boolean values, and the like. In TypeScript, we support the same types as you would ex-
pect in JavaScript, with an extra enumeration type thrown in to help things along.
Boolean
The most basic datatype is the simple true/false value, which JavaScript and TypeScript call a boolean
value.
Number
As in JavaScript, all numbers in TypeScript are either floating point values or BigIntegers. These floating
point numbers get the type number , while BigIntegers get the type bigint . In addition to hexadecimal
and decimal literals, TypeScript also supports binary and octal literals introduced in ECMAScript 2015.
// @target: ES2020
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;
String
Another fundamental part of creating programs in JavaScript for webpages and servers alike is working with
textual data. As in other languages, we use the type string to refer to these textual datatypes. Just like
JavaScript, TypeScript also uses double quotes ( " ) or single quotes ( ' ) to surround string data.
You can also use template strings, which can span multiple lines and have embedded expressions. These
strings are surrounded by the backtick/backquote ( ` ) character, and embedded expressions are of the
form ${ expr } .
83
typescript
Array
TypeScript, like JavaScript, allows you to work with arrays of values. Array types can be written in one of
two ways. In the first, you use the type of the elements followed by [] to denote an array of that element
type:
Tuple
Tuple types allow you to express an array with a fixed number of elements whose types are known, but
need not be the same. For example, you may want to represent a value as a pair of a string and a num‐
ber :
// @errors: 2322
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
When accessing an element with a known index, the correct type is retrieved:
// @errors: 2339
let x: [string, number];
x = ["hello", 10]; // OK
/// ---cut---
// OK
console.log(x[0].substring(1));
console.log(x[1].substring(1));
Accessing an element outside the set of known indices fails with an error:
console.log(x[5].toString());
84
Basic Types
Enum
A helpful addition to the standard set of datatypes from JavaScript is the enum . As in languages like C#, an
enum is a way of giving more friendly names to sets of numeric values.
enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green;
By default, enums begin numbering their members starting at 0 . You can change this by manually setting
the value of one of its members. For example, we can start the previous example at 1 instead of 0 :
enum Color {
Red = 1,
Green,
Blue,
}
let c: Color = Color.Green;
enum Color {
Red = 1,
Green = 2,
Blue = 4,
}
let c: Color = Color.Green;
A handy feature of enums is that you can also go from a numeric value to the name of that value in the
enum. For example, if we had the value 2 but weren't sure what that mapped to in the Color enum
above, we could look up the corresponding name:
enum Color {
Red = 1,
Green,
Blue,
}
let colorName: string = Color[2];
// Displays 'Green'
console.log(colorName);
Unknown
We may need to describe the type of variables that we do not know when we are writing an application.
These values may come from dynamic content – e.g. from the user – or we may want to intentionally ac-
cept all values in our API. In these cases, we want to provide a type that tells the compiler and future read-
ers that this variable could be anything, so we give it the unknown type.
85
typescript
If you have a variable with an unknown type, you can narrow it to something more specific by doing type‐
of checks, comparison checks, or more advanced type guards that will be discussed in a later chapter:
Any
In some situations, not all type information is available or its declaration would take an inappropriate
amount of effort. These may occur for values from code that has been written without TypeScript or a 3rd
party library. In these cases, we might want to opt-out of type checking. To do so, we label these values
with the any type:
The any type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-
out of type checking during compilation.
Unlike unknown , variables of type any allow you to access arbitrary properties, even ones that don't exist.
These properties include functions and TypeScript will not check their existence or type:
// @errors: 2571
let looselyTyped: any = 4;
// OK, ifItExists might exist at runtime
looselyTyped.ifItExists();
// OK, toFixed exists (but the compiler doesn't check)
looselyTyped.toFixed();
86
Basic Types
After all, remember that all the convenience of any comes at the cost of losing type safety. Type safety is
one of the main motivations for using TypeScript and you should try to avoid using any when not
necessary.
Void
void is a little like the opposite of any : the absence of having any type at all. You may commonly see this
as the return type of functions that do not return a value:
Declaring variables of type void is not useful because you can only assign null (only if strictNull‐
Checks is not specified, see next section) or undefined to them:
// @strict: false
let unusable: void = undefined;
// OK if `--strictNullChecks` is not given
unusable = null;
By default null and undefined are subtypes of all other types. That means you can assign null and
undefined to something like number .
However, when using the strictNullChecks flag, null and undefined are only assignable to unknown ,
any and their respective types (the one exception being that undefined is also assignable to void ). This
helps avoid many common errors. In cases where you want to pass in either a string or null or unde‐
fined , you can use the union type string | null | undefined .
Union types are an advanced topic that we'll cover in a later chapter.
87
typescript
As a note: we encourage the use of strictNullChecks when possible, but for the purposes of this
handbook, we will assume it is turned off.
Never
The never type represents the type of values that never occur. For instance, never is the return type for
a function expression or an arrow function expression that always throws an exception or one that never re-
turns. Variables also acquire the type never when narrowed by any type guards that can never be true.
The never type is a subtype of, and assignable to, every type; however, no type is a subtype of, or as-
signable to, never (except never itself). Even any isn't assignable to never .
Object
object is a type that represents the non-primitive type, i.e. anything that is not number , string , bool‐
ean , bigint , symbol , null , or undefined .
With object type, APIs like Object.create can be better represented. For example:
// @errors: 2345
declare function create(o: object | null): void;
// OK
create({ prop: 0 });
create(null);
create(undefined); // with `--strictNullChecks` flag enabled, undefined is not a
subtype of null
create(42);
create("string");
create(false);
88
Basic Types
Type assertions
Sometimes you'll end up in a situation where you'll know more about a value than TypeScript does. Usually,
this will happen when you know the type of some entity could be more specific than its current type.
Type assertions are a way to tell the compiler "trust me, I know what I'm doing." A type assertion is like a
type cast in other languages, but it performs no special checking or restructuring of data. It has no runtime
impact and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed
any special checks that you need.
The two samples are equivalent. Using one over the other is mostly a choice of preference; however, when
using TypeScript with JSX, only as -style assertions are allowed.
// @errors: 2339
function reverse(s: String): String {
return s.split("").reverse().join("");
}
reverse("hello world");
Instead, use the types number , string , boolean , object and symbol .
89
typescript
reverse("hello world");
Go to TOC
90
Classes
Traditional JavaScript uses functions and prototype-based inheritance to build up reusable components, but
this may feel a bit awkward to programmers more comfortable with an object-oriented approach, where
classes inherit functionality and objects are built from these classes. Starting with ECMAScript 2015, also
known as ECMAScript 6, JavaScript programmers can build their applications using this object-oriented
class-based approach. In TypeScript, we allow developers to use these techniques now, and compile them
down to JavaScript that works across all major browsers and platforms, without having to wait for the next
version of JavaScript.
Classes
Let's take a look at a simple class-based example:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
The syntax should look familiar if you've used C# or Java before. We declare a new class Greeter . This
class has three members: a property called greeting , a constructor, and a method greet .
You'll notice that in the class when we refer to one of the members of the class we prepend this. . This
denotes that it's a member access.
In the last line we construct an instance of the Greeter class using new . This calls into the constructor we
defined earlier, creating a new object with the Greeter shape, and running the constructor to initialize it.
Inheritance
In TypeScript, we can use common object-oriented patterns. One of the most fundamental patterns in class-
based programming is being able to extend existing classes to create new ones using inheritance.
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
91
typescript
This example shows the most basic inheritance feature: classes inherit properties and methods from base
classes. Here, Dog is a derived class that derives from the Animal base class using the extends
keyword. Derived classes are often called subclasses, and base classes are often called superclasses.
Because Dog extends the functionality from Animal , we were able to create an instance of Dog that could
both bark() and move() .
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
sam.move();
tom.move(34);
This example covers a few other features we didn't previously mention. Again, we see the extends key-
words used to create two new subclasses of Animal : Horse and Snake .
92
Classes
One difference from the prior example is that each derived class that contains a constructor function must
call super() which will execute the constructor of the base class. What's more, before we ever access a
property on this in a constructor body, we have to call super() . This is an important rule that TypeScript
will enforce.
The example also shows how to override methods in the base class with methods that are specialized for
the subclass. Here both Snake and Horse create a move method that overrides the move from Animal ,
giving it functionality specific to each class. Note that even though tom is declared as an Animal , since its
value is a Horse , calling tom.move(34) will call the overriding method in Horse :
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
You may still mark a member public explicitly. We could have written the Animal class from the previous
section in the following way:
class Animal {
public name: string;
// @errors: 18013
class Animal {
#name: string;
constructor(theName: string) {
this.#name = theName;
}
}
new Animal("Cat").#name;
93
typescript
This syntax is built into the JavaScript runtime and can have better guarantees about the isolation of each
private field. Right now, the best documentation for these private fields is in the TypeScript 3.8 release
notes.
// @errors: 2341
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
new Animal("Cat").name;
TypeScript is a structural type system. When we compare two different types, regardless of where they
came from, if the types of all members are compatible, then we say the types themselves are compatible.
However, when comparing types that have private and protected members, we treat these types differ-
ently. For two types to be considered compatible, if one of them has a private member, then the other
must have a private member that originated in the same declaration. The same applies to protected
members.
Let's look at an example to better see how this plays out in practice:
// @errors: 2322
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
class Employee {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
94
Classes
animal = rhino;
animal = employee;
In this example, we have an Animal and a Rhino , with Rhino being a subclass of Animal . We also have
a new class Employee that looks identical to Animal in terms of shape. We create some instances of these
classes and then try to assign them to each other to see what will happen. Because Animal and Rhino
share the private side of their shape from the same declaration of private name: string in Animal ,
they are compatible. However, this is not the case for Employee . When we try to assign from an Employee
to Animal we get an error that these types are not compatible. Even though Employee also has a pri‐
vate member called name , it's not the one we declared in Animal .
Understanding protected
The protected modifier acts much like the private modifier with the exception that members declared
protected can also be accessed within deriving classes. For example,
// @errors: 2445
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
Notice that while we can't use name from outside of Person , we can still use it from within an instance
method of Employee because Employee derives from Person .
A constructor may also be marked protected . This means that the class cannot be instantiated outside of
its containing class, but can be extended. For example,
// @errors: 2674
class Person {
protected name: string;
protected constructor(theName: string) {
this.name = theName;
}
}
95
typescript
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
Readonly modifier
You can make properties readonly by using the readonly keyword. Readonly properties must be initialized
at their declaration or in the constructor.
// @errors: 2540
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theName: string) {
this.name = theName;
}
}
Parameter properties
In our last example, we had to declare a readonly member name and a constructor parameter theName in
the Octopus class. This is needed in order to have the value of theName accessible after the Octopus
constructor is executed. Parameter properties let you create and initialize a member in one place. Here's a
further revision of the previous Octopus class using a parameter property:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {}
}
Notice how we dropped theName altogether and just use the shortened readonly name: string parame-
ter on the constructor to create and initialize the name member. We've consolidated the declarations and
assignment into one location.
96
Classes
Parameter properties are declared by prefixing a constructor parameter with an accessibility modifier or
readonly , or both. Using private for a parameter property declares and initializes a private member;
likewise, the same is done for public , protected , and readonly .
Accessors
TypeScript supports getters/setters as a way of intercepting accesses to a member of an object. This gives
you a way of having finer-grained control over how a member is accessed on each object.
Let's convert a simple class to use get and set . First, let's start with an example without getters and
setters.
// @strict: false
class Employee {
fullName: string;
}
if (employee.fullName) {
console.log(employee.fullName);
}
While allowing people to randomly set fullName directly is pretty handy, we may also want enforce some
constraints when fullName is set.
In this version, we add a setter that checks the length of the newName to make sure it's compatible with
the max-length of our backing database field. If it isn't we throw an error notifying client code that some-
thing went wrong.
To preserve existing functionality, we also add a simple getter that retrieves fullName unmodified.
// @strict: false
const fullNameMaxLength = 10;
class Employee {
private _fullName: string = "";
this._fullName = newName;
}
}
97
typescript
if (employee.fullName) {
console.log(employee.fullName);
}
To prove to ourselves that our accessor is now checking the length of values, we can attempt to assign a
name longer than 10 characters and verify that we get an error.
First, accessors require you to set the compiler to output ECMAScript 5 or higher. Downleveling to
ECMAScript 3 is not supported. Second, accessors with a get and no set are automatically inferred to be
readonly . This is helpful when generating a .d.ts file from your code, because users of your property
can see that they can't change it.
Static Properties
Up to this point, we've only talked about the instance members of the class, those that show up on the ob-
ject when it's instantiated. We can also create static members of a class, those that are visible on the class
itself rather than on the instances. In this example, we use static on the origin, as it's a general value for
all grids. Each instance accesses this value through prepending the name of the class. Similarly to prepend-
ing this. in front of instance accesses, here we prepend Grid. in front of static accesses.
class Grid {
static origin = { x: 0, y: 0 };
Abstract Classes
Abstract classes are base classes from which other classes may be derived. They may not be instantiated
directly. Unlike an interface, an abstract class may contain implementation details for its members. The ab‐
stract keyword is used to define abstract classes as well as abstract methods within an abstract class.
move(): void {
console.log("roaming the earth...");
}
}
98
Classes
Methods within an abstract class that are marked as abstract do not contain an implementation and must be
implemented in derived classes. Abstract methods share a similar syntax to interface methods. Both define
the signature of a method without including a method body. However, abstract methods must include the
abstract keyword and may optionally include access modifiers.
printName(): void {
console.log("Department name: " + this.name);
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
Advanced Techniques
Constructor functions
When you declare a class in TypeScript, you are actually creating multiple declarations at the same time.
The first is the type of the instance of the class.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
99
typescript
}
}
Here, when we say let greeter: Greeter , we're using Greeter as the type of instances of the class
Greeter . This is almost second nature to programmers from other object-oriented languages.
We're also creating another value that we call the constructor function. This is the function that is called
when we new up instances of the class. To see what this looks like in practice, let's take a look at the
JavaScript created by the above example:
// @strict: false
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"
Here, let Greeter is going to be assigned the constructor function. When we call new and run this func-
tion, we get an instance of the class. The constructor function also contains all of the static members of the
class. Another way to think of each class is that there is an instance side and a static side.
// @strict: false
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
} else {
return Greeter.standardGreeting;
}
}
}
100
Classes
In this example, greeter1 works similarly to before. We instantiate the Greeter class, and use this ob-
ject. This we have seen before.
Next, we then use the class directly. Here we create a new variable called greeterMaker . This variable will
hold the class itself, or said another way its constructor function. Here we use typeof Greeter , that is
"give me the type of the Greeter class itself" rather than the instance type. Or, more precisely, "give me
the type of the symbol called Greeter ," which is the type of the constructor function. This type will contain
all of the static members of Greeter along with the constructor that creates instances of the Greeter class.
We show this by using new on greeterMaker , creating new instances of Greeter and invoking them as
before. It is also good to mention that changing static property is frowned upon, here greeter3 has "Hey
there!" instead of "Hello, there" on standardGreeting .
// @strict: false
class Point {
x: number;
y: number;
}
Go to TOC
101
typescript
Functions are the fundamental building block of any application in JavaScript. They're how you build up lay-
ers of abstraction, mimicking classes, information hiding, and modules. In TypeScript, while there are class-
es, namespaces, and modules, functions still play the key role in describing how to do things. TypeScript
also adds some new capabilities to the standard JavaScript functions to make them easier to work with.
Functions
To begin, just as in JavaScript, TypeScript functions can be created both as a named function or as an
anonymous function. This allows you to choose the most appropriate approach for your application, whether
you're building a list of functions in an API or a one-off function to hand off to another function.
// @strict: false
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function (x, y) {
return x + y;
};
Just as in JavaScript, functions can refer to variables outside of the function body. When they do so, they're
said to capture these variables. While understanding how this works (and the trade-offs when using this
technique) is outside of the scope of this article, having a firm understanding how this mechanic works is an
important piece of working with JavaScript and TypeScript.
// @strict: false
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
Function Types
Typing the function
Let's add types to our simple examples from earlier:
102
Functions
We can add types to each of the parameters and then to the function itself to add a return type. TypeScript
can figure the return type out by looking at the return statements, so we can also optionally leave this off in
many cases.
A function's type has the same two parts: the type of the arguments and the return type. When writing out
the whole function type, both parts are required. We write out the parameter types just like a parameter
list, giving each parameter a name and a type. This name is just to help with readability. We could have in-
stead written:
As long as the parameter types line up, it's considered a valid type for the function, regardless of the names
you give the parameters in the function type.
The second part is the return type. We make it clear which is the return type by using an arrow ( => ) be-
tween the parameters and the return type. As mentioned before, this is a required part of the function type,
so if the function doesn't return a value, you would use void instead of leaving it off.
Of note, only the parameters and the return type make up the function type. Captured variables are not re-
flected in the type. In effect, captured variables are part of the "hidden state" of any function and do not
make up its API.
103
typescript
let myAdd2: (baseValue: number, increment: number) => number = function (x, y) {
return x + y;
};
This is called "contextual typing", a form of type inference. This helps cut down on the amount of effort to
keep your program typed.
// @errors: 2554
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
In JavaScript, every parameter is optional, and users may leave them off as they see fit. When they do,
their value is undefined . We can get this functionality in TypeScript by adding a ? to the end of parame-
ters we want to be optional. For example, let's say we want the last name parameter from above to be
optional:
// @errors: 2554
function buildName(firstName: string, lastName?: string) {
if (lastName) return firstName + " " + lastName;
else return firstName;
}
Any optional parameters must follow required parameters. Had we wanted to make the first name optional,
rather than the last name, we would need to change the order of parameters in the function, putting the
first name last in the list.
In TypeScript, we can also set a value that a parameter will be assigned if the user does not provide one, or
if the user passes undefined in its place. These are called default-initialized parameters. Let's take the
previous example and default the last name to "Smith" .
// @errors: 2554
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
104
Functions
Default-initialized parameters that come after all required parameters are treated as optional, and just like
optional parameters, can be omitted when calling their respective function. This means optional parameters
and trailing default parameters will share commonality in their types, so both
and
share the same type (firstName: string, lastName?: string) => string . The default value of last‐
Name disappears in the type, only leaving behind the fact that the parameter is optional.
Unlike plain optional parameters, default-initialized parameters don't need to occur after required parame-
ters. If a default-initialized parameter comes before a required parameter, users need to explicitly pass un‐
defined to get the default initialized value. For example, we could write our last example with only a de-
fault initializer on firstName :
// @errors: 2554
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
Rest Parameters
Required, optional, and default parameters all have one thing in common: they talk about one parameter at
a time. Sometimes, you want to work with multiple parameters as a group, or you may not know how many
parameters a function will ultimately take. In JavaScript, you can work with the arguments directly using
the arguments variable that is visible inside every function body.
105
typescript
Rest parameters are treated as a boundless number of optional parameters. When passing arguments for a
rest parameter, you can use as many as you want; you can even pass none. The compiler will build an array
of the arguments passed in with the name given after the ellipsis ( ... ), allowing you to use it in your
function.
The ellipsis is also used in the type of the function with rest parameters:
this
Learning how to use this in JavaScript is something of a rite of passage. Since TypeScript is a superset of
JavaScript, TypeScript developers also need to learn how to use this and how to spot when it's not being
used correctly. Fortunately, TypeScript lets you catch incorrect uses of this with a couple of techniques. If
you need to learn how this works in JavaScript, though, first read Yehuda Katz's Understanding JavaScript
Function Invocation and "this". Yehuda's article explains the inner workings of this very well, so we'll just
cover the basics here.
// @strict: false
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function () {
return function () {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
106
Functions
Notice that createCardPicker is a function that itself returns a function. If we tried to run the example,
we would get an error instead of the expected alert box. This is because the this being used in the func-
tion created by createCardPicker will be set to window instead of our deck object. That's because we
call cardPicker() on its own. A top-level non-method syntax call like this will use window for this .
(Note: under strict mode, this will be undefined rather than window ).
We can fix this by making sure the function is bound to the correct this before we return the function to
be used later. This way, regardless of how it's later used, it will still be able to see the original deck object.
To do this, we change the function expression to use the ECMAScript 6 arrow syntax. Arrow functions cap-
ture the this where the function is created rather than where it is invoked:
// @strict: false
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function () {
// NOTE: the line below is now an arrow function, allowing us to capture
'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
Even better, TypeScript will warn you when you make this mistake if you pass the noImplicitThis flag to
the compiler. It will point out that this in this.suits[pickedSuit] is of type any .
this parameters
Unfortunately, the type of this.suits[pickedSuit] is still any . That's because this comes from the
function expression inside the object literal. To fix this, you can provide an explicit this parameter. this
parameters are fake parameters that come first in the parameter list of a function:
Let's add a couple of interfaces to our example above, Card and Deck , to make the types clearer and eas-
ier to reuse:
interface Card {
suit: string;
card: number;
}
107
typescript
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
Now TypeScript knows that createCardPicker expects to be called on a Deck object. That means that
this is of type Deck now, not any , so noImplicitThis will not cause any errors.
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void means that addClickListener expects onclick to be a function that does not require a
this type. Second, annotate your calling code with this :
// @strict: false
// @errors: 2345
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
interface Event {
message: string;
}
declare const uiElement: UIElement;
// ---cut---
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// oops, used `this` here. using this callback would crash at runtime
108
Functions
this.info = e.message;
}
}
With this annotated, you make it explicit that onClickBad must be called on an instance of Handler .
Then TypeScript will detect that addClickListener requires a function that has this: void . To fix the
error, change the type of this :
// @strict: false
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
interface Event {
message: string;
}
declare const uiElement: UIElement;
// ---cut---
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can't use `this` here because it's of type void!
console.log("clicked!");
}
}
Because onClickGood specifies its this type as void , it is legal to pass to addClickListener . Of
course, this also means that it can't use this.info . If you want both then you'll have to use an arrow
function:
// @strict: false
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
interface Event {
message: string;
}
declare const uiElement: UIElement;
// ---cut---
class Handler {
info: string;
onClickGood = (e: Event) => {
this.info = e.message;
};
}
This works because arrow functions use the outer this , so you can always pass them to something that
expects this: void . The downside is that one arrow function is created per object of type Handler.
Methods, on the other hand, are only created once and attached to Handler's prototype. They are shared
between all objects of type Handler.
109
typescript
Overloads
JavaScript is inherently a very dynamic language. It's not uncommon for a single JavaScript function to re-
turn different types of objects based on the shape of the arguments passed in.
// @strict: false
let suits = ["hearts", "spades", "clubs", "diamonds"];
let myDeck = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 },
];
Here, the pickCard function will return two different things based on what the user has passed in. If the
users passes in an object that represents the deck, the function will pick the card. If the user picks the card,
we tell them which card they've picked. But how do we describe this to the type system?
The answer is to supply multiple function types for the same function as a list of overloads. This list is what
the compiler will use to resolve function calls. Let's create a list of overloads that describe what our pick‐
Card accepts and what it returns.
110
Functions
}
}
let myDeck = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 },
];
With this change, the overloads now give us type checked calls to the pickCard function.
In order for the compiler to pick the correct type check, it follows a similar process to the underlying
JavaScript. It looks at the overload list and, proceeding with the first overload, attempts to call the function
with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this rea-
son, it's customary to order overloads from most specific to least specific.
Note that the function pickCard(x): any piece is not part of the overload list, so it only has two over-
loads: one that takes an object and one that takes a number. Calling pickCard with any other parameter
types would cause an error.
Go to TOC
111
typescript
A major part of software engineering is building components that not only have well-defined and consistent
APIs, but are also reusable. Components that are capable of working on the data of today as well as the
data of tomorrow will give you the most flexible capabilities for building up large software systems.
In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is
generics, that is, being able to create a component that can work over a variety of types rather than a sin-
gle one. This allows users to consume these components and use their own types.
Without generics, we would either have to give the identity function a specific type:
Or, we could describe the identity function using the any type:
While using any is certainly generic in that it will cause the function to accept any and all types for the type
of arg , we actually are losing the information about what that type was when the function returns. If we
passed in a number, the only information we have is that any type could be returned.
Instead, we need a way of capturing the type of the argument in such a way that we can also use it to de-
note what is being returned. Here, we will use a type variable, a special kind of variable that works on types
rather than values.
We've now added a type variable T to the identity function. This T allows us to capture the type the user
provides (e.g. number ), so that we can use that information later. Here, we use T again as the return
type. On inspection, we can now see the same type is used for the argument and the return type. This al-
lows us to traffic that type information in one side of the function and out the other.
We say that this version of the identity function is generic, as it works over a range of types. Unlike us-
ing any , it's also just as precise (ie, it doesn't lose any information) as the first identity function that
used numbers for the argument and return type.
Once we've written the generic identity function, we can call it in one of two ways. The first way is to pass
all of the arguments, including the type argument, to the function:
112
Generics
Here we explicitly set T to be string as one of the arguments to the function call, denoted using the <>
around the arguments rather than () .
The second way is also perhaps the most common. Here we use type argument inference -- that is, we
want the compiler to set the value of T for us automatically based on the type of the argument we pass in:
Notice that we didn't have to explicitly pass the type in the angle brackets ( <> ); the compiler just looked at
the value "myString" , and set T to its type. While type argument inference can be a helpful tool to keep
code shorter and more readable, you may need to explicitly pass in the type arguments as we did in the
previous example when the compiler fails to infer the type, as may happen in more complex examples.
What if we want to also log the length of the argument arg to the console with each call? We might be
tempted to write this:
// @errors: 2339
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
When we do, the compiler will give us an error that we're using the .length member of arg , but nowhere
have we said that arg has this member. Remember, we said earlier that these type variables stand in for
any and all types, so someone using this function could have passed in a number instead, which does not
have a .length member.
113
typescript
Let's say that we've actually intended this function to work on arrays of T rather than T directly. Since
we're working with arrays, the .length member should be available. We can describe this just like we
would create arrays of other types:
You can read the type of loggingIdentity as "the generic function loggingIdentity takes a type para-
meter T , and an argument arg which is an array of T s, and returns an array of T s." If we passed in an
array of numbers, we'd get an array of numbers back out, as T would bind to number . This allows us to
use our generic type variable T as part of the types we're working with, rather than the whole type, giving
us greater flexibility.
You may already be familiar with this style of type from other languages. In the next section, we'll cover
how you can create your own generic types like Array<T> .
Generic Types
In previous sections, we created generic identity functions that worked over a range of types. In this
section, we'll explore the type of the functions themselves and how to create generic interfaces.
The type of generic functions is just like those of non-generic functions, with the type parameters listed
first, similarly to function declarations:
We could also have used a different name for the generic type parameter in the type, so long as the number
of type variables and how the type variables are used line up.
We can also write the generic type as a call signature of an object literal type:
114
Generics
Which leads us to writing our first generic interface. Let's take the object literal from the previous example
and move it to an interface:
interface GenericIdentityFn {
<T>(arg: T): T;
}
In a similar example, we may want to move the generic parameter to be a parameter of the whole
interface. This lets us see what type(s) we're generic over (e.g. Dictionary<string> rather than just
Dictionary ). This makes the type parameter visible to all the other members of the interface.
interface GenericIdentityFn<T> {
(arg: T): T;
}
Notice that our example has changed to be something slightly different. Instead of describing a generic
function, we now have a non-generic function signature that is a part of a generic type. When we use
GenericIdentityFn , we now will also need to specify the corresponding type argument (here: number ),
effectively locking in what the underlying call signature will use. Understanding when to put the type para-
meter directly on the call signature and when to put it on the interface itself will be helpful in describing
what aspects of a type are generic.
In addition to generic interfaces, we can also create generic classes. Note that it is not possible to create
generic enums and namespaces.
Generic Classes
A generic class has a similar shape to a generic interface. Generic classes have a generic type parameter list
in angle brackets ( <> ) following the name of the class.
// @strict: false
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
115
typescript
This is a pretty literal use of the GenericNumber class, but you may have noticed that nothing is restricting
it to only use the number type. We could have instead used string or even more complex objects.
// @strict: false
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
// ---cut---
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
Just as with interface, putting the type parameter on the class itself lets us make sure all of the properties
of the class are working with the same type.
As we covered in our section on classes, a class has two sides to its type: the static side and the instance
side. Generic classes are only generic over their instance side rather than their static side, so when working
with classes, static members can not use the class's type parameter.
Generic Constraints
If you remember from an earlier example, you may sometimes want to write a generic function that works
on a set of types where you have some knowledge about what capabilities that set of types will have. In our
loggingIdentity example, we wanted to be able to access the .length property of arg , but the com-
piler could not prove that every type had a .length property, so it warns us that we can't make this
assumption.
// @errors: 2339
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
Instead of working with any and all types, we'd like to constrain this function to work with any and all types
that also have the .length property. As long as the type has this member, we'll allow it, but it's required
to have at least this member. To do so, we must list our requirement as a constraint on what T can be.
To do so, we'll create an interface that describes our constraint. Here, we'll create an interface that has a
single .length property and then we'll use this interface and the extends keyword to denote our
constraint:
116
Generics
interface Lengthwise {
length: number;
}
Because the generic function is now constrained, it will no longer work over any and all types:
// @errors: 2345
interface Lengthwise {
length: number;
}
Instead, we need to pass in values whose type has all the required properties:
interface Lengthwise {
length: number;
}
// @errors: 2345
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
117
typescript
A more advanced example uses the prototype property to infer and constrain relationships between the
constructor function and the instance side of class types.
// @strict: false
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Go to TOC
118
Interfaces
One of TypeScript's core principles is that type checking focuses on the shape that values have. This is
sometimes called "duck typing" or "structural subtyping". In TypeScript, interfaces fill the role of naming
these types, and are a powerful way of defining contracts within your code as well as contracts with code
outside of your project.
The type checker checks the call to printLabel . The printLabel function has a single parameter that re-
quires that the object passed in has a property called label of type string . Notice that our object actual-
ly has more properties than this, but the compiler only checks that at least the ones required are present
and match the types required. There are some cases where TypeScript isn't as lenient, which we'll cover in a
bit.
We can write the same example again, this time using an interface to describe the requirement of having
the label property that is a string:
interface LabeledValue {
label: string;
}
The interface LabeledValue is a name we can now use to describe the requirement in the previous exam-
ple. It still represents having a single property called label that is of type string . Notice we didn't have
to explicitly say that the object we pass to printLabel implements this interface like we might have to in
other languages. Here, it's only the shape that matters. If the object we pass to the function meets the re-
quirements listed, then it's allowed.
It's worth pointing out that the type checker does not require that these properties come in any sort of or-
der, only that the properties the interface requires are present and have the required type.
119
typescript
Optional Properties
Not all properties of an interface may be required. Some exist under certain conditions or may not be there
at all. These optional properties are popular when creating patterns like "option bags" where you pass an
object to a function that only has a couple of properties filled in.
interface SquareConfig {
color?: string;
width?: number;
}
Interfaces with optional properties are written similar to other interfaces, with each optional property denot-
ed by a ? at the end of the property name in the declaration.
The advantage of optional properties is that you can describe these possibly available properties while still
also preventing use of properties that are not part of the interface. For example, had we mistyped the name
of the color property in createSquare , we would get an error message letting us know:
// @errors: 2551
interface SquareConfig {
color?: string;
width?: number;
}
120
Interfaces
Readonly properties
Some properties should only be modifiable when an object is first created. You can specify this by putting
readonly before the name of the property:
interface Point {
readonly x: number;
readonly y: number;
}
You can construct a Point by assigning an object literal. After the assignment, x and y can't be
changed.
// @errors: 2540
interface Point {
readonly x: number;
readonly y: number;
}
// ---cut---
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript comes with a ReadonlyArray<T> type that is the same as Array<T> with all mutating methods
removed, so you can make sure you don't change your arrays after creation:
On the last line of the snippet you can see that even assigning the entire ReadonlyArray back to a normal
array is illegal. You can still override it with a type assertion, though:
a = ro as number[];
readonly vs const
The easiest way to remember whether to use readonly or const is to ask whether you're using it on a
variable or a property. Variables use const whereas properties use readonly .
121
typescript
However, combining the two naively would allow an error to sneak in. For example, taking our last example
using createSquare :
Notice the given argument to createSquare is spelled colour instead of color . In plain JavaScript, this
sort of thing fails silently.
You could argue that this program is correctly typed, since the width properties are compatible, there's no
color property present, and the extra colour property is insignificant.
However, TypeScript takes the stance that there's probably a bug in this code. Object literals get special
treatment and undergo excess property checking when assigning them to other variables, or passing them
as arguments. If an object literal has any properties that the "target type" doesn't have, you'll get an error:
Getting around these checks is actually really simple. The easiest method is to just use a type assertion:
122
Interfaces
}
// ---cut---
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
However, a better approach might be to add a string index signature if you're sure that the object can have
some extra properties that are used in some special way. If SquareConfig can have color and width
properties with the above types, but could also have any number of other properties, then we could define it
like so:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
We'll discuss index signatures in a bit, but here we're saying a SquareConfig can have any number of
properties, and as long as they aren't color or width , their types don't matter.
One final way to get around these checks, which might be a bit surprising, is to assign the object to another
variable: Since squareOptions won't undergo excess property checks, the compiler won't give you an
error.
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
The above workaround will work as long as you have a common property between squareOptions and
SquareConfig . In this example, it was the property width . It will however, fail if the variable does not
have any common object property. For example:
// @errors: 2559
interface SquareConfig {
color?: string;
width?: number;
}
123
typescript
// ---cut---
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Keep in mind that for simple code like above, you probably shouldn't be trying to "get around" these checks.
For more complex object literals that have methods and hold state, you might need to keep these tech-
niques in mind, but a majority of excess property errors are actually bugs. That means if you're running into
excess property checking problems for something like option bags, you might need to revise some of your
type declarations. In this instance, if it's okay to pass an object with both a color or colour property to
createSquare , you should fix up the definition of SquareConfig to reflect that.
Function Types
Interfaces are capable of describing the wide range of shapes that JavaScript objects can take. In addition
to describing an object with properties, interfaces are also capable of describing function types.
To describe a function type with an interface, we give the interface a call signature. This is like a function
declaration with only the parameter list and return type given. Each parameter in the parameter list re-
quires both name and type.
interface SearchFunc {
(source: string, subString: string): boolean;
}
Once defined, we can use this function type interface like we would other interfaces. Here, we show how
you can create a variable of a function type and assign it a function value of the same type.
interface SearchFunc {
(source: string, subString: string): boolean;
}
// ---cut---
let mySearch: SearchFunc;
For function types to correctly type check, the names of the parameters do not need to match. We could
have, for example, written the above example like this:
interface SearchFunc {
(source: string, subString: string): boolean;
}
// ---cut---
let mySearch: SearchFunc;
124
Interfaces
Function parameters are checked one at a time, with the type in each corresponding parameter position
checked against each other. If you do not want to specify types at all, TypeScript's contextual typing can in-
fer the argument types since the function value is assigned directly to a variable of type SearchFunc . Here,
also, the return type of our function expression is implied by the values it returns (here false and true ).
interface SearchFunc {
(source: string, subString: string): boolean;
}
// ---cut---
let mySearch: SearchFunc;
Had the function expression returned numbers or strings, the type checker would have made an error that
indicates return type doesn't match the return type described in the SearchFunc interface.
// @errors: 2322
interface SearchFunc {
(source: string, subString: string): boolean;
}
// ---cut---
let mySearch: SearchFunc;
Indexable Types
Similarly to how we can use interfaces to describe function types, we can also describe types that we can
"index into" like a[10] , or ageMap["daniel"] . Indexable types have an index signature that describes
the types we can use to index into the object, along with the corresponding return types when indexing.
interface StringArray {
[index: number]: string;
}
Above, we have a StringArray interface that has an index signature. This index signature states that
when a StringArray is indexed with a number , it will return a string .
There are four types of supported index signatures: string, number, symbol and template strings. It is pos-
sible to support many types of indexers, but the type returned from a numeric indexer must be a subtype of
the type returned from the string indexer.
125
typescript
This is because when indexing with a number , JavaScript will actually convert that to a string before in-
dexing into an object. That means that indexing with 100 (a number ) is the same thing as indexing with
"100" (a string ), so the two need to be consistent.
// @errors: 2413
// @strictPropertyInitialization: false
interface Animal {
name: string;
}
// Error: indexing with a numeric string might get you a completely separate type
of Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that
all properties match their return type. This is because a string index declares that obj.property is also
available as obj["property"] . In the following example, name 's type does not match the string index's
type, and the type checker gives an error:
// @errors: 2411
interface NumberDictionary {
[index: string]: number;
However, properties of different types are acceptable if the index signature is a union of the property types:
interface NumberOrStringDictionary {
[index: string]: number | string;
Finally, you can make index signatures readonly in order to prevent assignment to their indices:
// @errors: 2542
interface ReadonlyStringArray {
readonly [index: number]: string;
}
126
Interfaces
// @errors: 2339
interface HeadersResponse {
"content-type": string,
date: string,
"content-length": string
Class Types
Implementing an interface
One of the most common uses of interfaces in languages like C# and Java, that of explicitly enforcing that a
class meets a particular contract, is also possible in TypeScript.
interface ClockInterface {
currentTime: Date;
}
You can also describe methods in an interface that are implemented in the class, as we do with setTime in
the below example:
// @strictPropertyInitialization: false
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
127
typescript
}
constructor(h: number, m: number) {}
}
Interfaces describe the public side of the class, rather than both the public and private side. This prohibits
you from using them to check that a class also has particular types for the private side of the class instance.
This is because when a class implements an interface, only the instance side of the class is checked. Since
the constructor sits in the static side, it is not included in this check.
Instead, you would need to work with the static side of the class directly. In this example, we define two in-
terfaces, ClockConstructor for the constructor and ClockInterface for the instance methods. Then, for
convenience, we define a constructor function createClock that creates instances of the type that is
passed to it:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
128
Interfaces
// @strictPropertyInitialization: false
// @noImplicitAny: false
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
Extending Interfaces
Like classes, interfaces can extend each other. This allows you to copy the members of one interface into
another, which gives you more flexibility in how you separate your interfaces into reusable components.
interface Shape {
color: string;
}
An interface can extend multiple interfaces, creating a combination of all of the interfaces.
interface Shape {
color: string;
}
129
typescript
interface PenStroke {
penWidth: number;
}
Hybrid Types
As we mentioned earlier, interfaces can describe the rich types present in real world JavaScript. Because of
JavaScript's dynamic and flexible nature, you may occasionally encounter an object that works as a combi-
nation of some of the types described above.
One such example is an object that acts as both a function and an object, with additional properties:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
When interacting with 3rd-party JavaScript, you may need to use patterns like the above to fully describe
the shape of the type.
This is useful when you have a large inheritance hierarchy, but want to specify that your code works with
only subclasses that have certain properties. The subclasses don't have to be related besides inheriting from
the base class. For example:
130
Interfaces
In the above example, SelectableControl contains all of the members of Control , including the private
state property. Since state is a private member it is only possible for descendants of Control to imple-
ment SelectableControl . This is because only descendants of Control will have a state private mem-
ber that originates in the same declaration, which is a requirement for private members to be compatible.
Within the Control class it is possible to access the state private member through an instance of
SelectableControl . Effectively, a SelectableControl acts like a Control that is known to have a se‐
lect method. The Button and TextBox classes are subtypes of SelectableControl (because they both
inherit from Control and have a select method). The ImageControl class has its own state private
member rather than extending Control , so it cannot implement SelectableControl .
Go to TOC
131
typescript
A literal is a more concrete sub-type of a collective type. What this means is that "Hello World" is a
string , but a string is not "Hello World" inside the type system.
There are three sets of literal types available in TypeScript today: strings, numbers, and booleans; by using
literal types you can allow an exact value which a string, number, or boolean must have.
Literal Narrowing
When you declare a variable via var or let , you are telling the compiler that there is the chance that this
variable will change its contents. In contrast, using const to declare a variable will inform TypeScript that
this object will never change.
// On the other hand, a let can change, and so the compiler declares it a string
let hiWorld = "Hi World";
The process of going from an infinite number of potential cases (there is an infinite number of possible
string values) to a smaller, finite number of potential case (in helloWorld 's case: 1) is called narrowing.
// @errors: 2345
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
} else if (easing === "ease-out") {
} else if (easing === "ease-in-out") {
} else {
// It's possible that someone could reach this
// by ignoring your types though.
}
}
}
You can pass any of the three allowed strings, but any other string will give the error
132
Literal Types
String literal types can be used in the same way to distinguish overloads:
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
return (Math.floor(Math.random() * 6) + 1) as 1 | 2 | 3 | 4 | 5 | 6;
}
interface ValidationSuccess {
isValid: true;
reason: null;
}
interface ValidationFailure {
isValid: false;
reason: string;
}
Go to TOC
133
typescript
So far, the handbook has covered types which are atomic objects. However, as you model more types you
find yourself looking for tools which let you compose or combine existing types instead of creating them
from scratch.
Intersection and Union types are one of the ways in which you can compose types.
Union Types
Occasionally, you'll run into a library that expects a parameter to be either a number or a string . For in-
stance, take the following function:
/**
* Takes a string and adds "padding" to the left.
* If 'padding' is a string, then 'padding' is appended to the left side.
* If 'padding' is a number, then that number of spaces is added to the left side.
*/
function padLeft(value: string, padding: any) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${typeof padding}'.`);
}
The problem with padLeft in the above example is that its padding parameter is typed as any . That
means that we can call it with an argument that's neither a number nor a string , but TypeScript will be
okay with it.
In traditional object-oriented code, we might abstract over the two types by creating a hierarchy of types.
While this is much more explicit, it's also a little bit overkill. One of the nice things about the original version
of padLeft was that we were able to just pass in primitives. That meant that usage was simple and con-
cise. This new approach also wouldn't help if we were just trying to use a function that already exists
elsewhere.
Instead of any , we can use a union type for the padding parameter:
// @errors: 2345
/**
* Takes a string and adds "padding" to the left.
* If 'padding' is a string, then 'padding' is appended to the left side.
* If 'padding' is a number, then that number of spaces is added to the left side.
*/
function padLeft(value: string, padding: string | number) {
// ...
134
Unions and Intersection Types
A union type describes a value that can be one of several types. We use the vertical bar ( | ) to separate
each type, so number | string | boolean is the type of a value that can be a number , a string , or a
boolean .
// @errors: 2339
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
Union types can be a bit tricky here, but it just takes a bit of intuition to get used to. If a value has the type
A | B , we only know for certain that it has members that both A and B have. In this example, Bird has
a member named fly . We can't be sure whether a variable typed as Bird | Fish has a fly method. If
the variable is really a Fish at runtime, then calling pet.fly() will fail.
Discriminating Unions
A common technique for working with unions is to have a single field which uses literal types which you can
use to let TypeScript narrow down the possible current type. For example, we're going to create a union of
three types which have a single shared field.
type NetworkLoadingState = {
state: "loading";
};
type NetworkFailedState = {
state: "failed";
code: number;
};
type NetworkSuccessState = {
135
typescript
state: "success";
response: {
title: string;
duration: number;
summary: string;
};
};
All of the above types have a field named state , and then they also have their own fields:
code response
Given the state field is common in every type inside NetworkState - it is safe for your code to access
without an existence check.
With state as a literal type, you can compare the value of state to the equivalent string and TypeScript
will know which type is currently being used.
In this case, you can use a switch statement to narrow down which type is represented at runtime:
// @errors: 2339
type NetworkLoadingState = {
state: "loading";
};
type NetworkFailedState = {
state: "failed";
code: number;
};
type NetworkSuccessState = {
state: "success";
response: {
title: string;
duration: number;
summary: string;
};
};
// ---cut---
type NetworkState =
| NetworkLoadingState
136
Unions and Intersection Types
| NetworkFailedState
| NetworkSuccessState;
// @errors: 2366
type NetworkLoadingState = { state: "loading" };
type NetworkFailedState = { state: "failed"; code: number };
type NetworkSuccessState = {
state: "success";
response: {
title: string;
duration: number;
summary: string;
};
};
// ---cut---
type NetworkFromCachedState = {
state: "from_cache";
id: string;
response: NetworkSuccessState["response"];
};
type NetworkState =
| NetworkLoadingState
| NetworkFailedState
| NetworkSuccessState
| NetworkFromCachedState;
137
typescript
case "success":
return "got response";
}
}
There are two ways to do this. The first is to turn on strictNullChecks and specify a return type:
// @errors: 2366
type NetworkLoadingState = { state: "loading" };
type NetworkFailedState = { state: "failed"; code: number };
type NetworkSuccessState = { state: "success" };
type NetworkFromCachedState = { state: "from_cache" };
type NetworkState =
| NetworkLoadingState
| NetworkFailedState
| NetworkSuccessState
| NetworkFromCachedState;
// ---cut---
function logger(s: NetworkState): string {
switch (s.state) {
case "loading":
return "loading request";
case "failed":
return `failed with code ${s.code}`;
case "success":
return "got response";
}
}
Because the switch is no longer exhaustive, TypeScript is aware that the function could sometimes return
undefined . If you have an explicit return type string , then you will get an error that the return type is
actually string | undefined . However, this method is quite subtle and, besides, strictNullChecks
does not always work with old code.
The second method uses the never type that the compiler uses to check for exhaustiveness:
// @errors: 2345
type NetworkLoadingState = { state: "loading" };
type NetworkFailedState = { state: "failed"; code: number };
type NetworkSuccessState = { state: "success" };
type NetworkFromCachedState = { state: "from_cache" };
type NetworkState =
| NetworkLoadingState
| NetworkFailedState
| NetworkSuccessState
| NetworkFromCachedState;
// ---cut---
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
138
Unions and Intersection Types
Here, assertNever checks that s is of type never — the type that's left after all other cases have been
removed. If you forget a case, then s will have a real type and you will get a type error. This method re-
quires you to define an extra function, but it's much more obvious when you forget it because the error
message includes the missing type name.
Intersection Types
Intersection types are closely related to union types, but they are used very differently. An intersection type
combines multiple types into one. This allows you to add together existing types to get a single type that
has all the features you need. For example, Person & Serializable & Loggable is a type which is all of
Person and Serializable and Loggable . That means an object of this type will have all members of all
three types.
For example, if you had networking requests with consistent error handling then you could separate out the
error handling into its own type which is merged with types which correspond to a single response type.
interface ErrorHandling {
success: boolean;
error?: { message: string };
}
interface ArtworksData {
artworks: { title: string }[];
}
interface ArtistsData {
artists: { name: string }[];
}
console.log(response.artists);
};
Go to TOC
139
typescript
Each and every value in JavaScript has a set of behaviors you can observe from running different
operations. That sounds abstract, but as a quick example, consider some operations we might run on a vari-
able named message .
// Calling 'message'
message();
If we break this down, the first runnable line of code accesses a property called toLowerCase and then
calls it. The second one tries to call message directly.
But assuming we don't know the value of message - and that's pretty common - we can't reliably say what
results we'll get from trying to run any of this code. The behavior of each operation depends entirely on
what value we had in the first place.
Is message callable?
Does it have a property called toLowerCase on it?
If it does, is toLowerCase even callable?
If both of these values are callable, what do they return?
The answers to these questions are usually things we keep in our heads when we write JavaScript, and we
have to hope we got all the details right.
As you can probably guess, if we try to run message.toLowerCase() , we'll get the same string only in low-
er-case.
What about that second line of code? If you're familiar with JavaScript, you'll know this fails with an
exception:
When we run our code, the way that our JavaScript runtime chooses what to do is by figuring out the type
of the value - what sorts of behaviors and capabilities it has. That's part of what that TypeError is alluding
to - it's saying that the string "Hello World!" cannot be called as a function.
For some values, such as the primitives string and number , we can identify their type at runtime using
the typeof operator. But for other things like functions, there's no corresponding runtime mechanism to
identify their types. For example, consider this function:
140
The Basics
function fn(x) {
return x.flip();
}
We can observe by reading the code that this function will only work if given an object with a callable flip
property, but JavaScript doesn't surface this information in a way that we can check while the code is run-
ning. The only way in pure JavaScript to tell what fn does with a particular value is to call it and see what
happens. This kind of behavior makes it hard to predict what code will do before it runs, which means it's
harder to know what your code is going to do while you're writing it.
Seen in this way, a type is the concept of describing which values can be passed to fn and which will
crash. JavaScript only truly provides dynamic typing - running the code to see what happens.
The alternative is to use a static type system to make predictions about what code is expected before it
runs.
Static type-checking
Think back to that TypeError we got earlier from trying to call a string as a function. Most people don't
like to get any sorts of errors when running their code - those are considered bugs! And when we write new
code, we try our best to avoid introducing new bugs.
If we add just a bit of code, save our file, re-run the code, and immediately see the error, we might be able
to isolate the problem quickly; but that's not always the case. We might not have tested the feature thor-
oughly enough, so we might never actually run into a potential error that would be thrown! Or if we were
lucky enough to witness the error, we might have ended up doing large refactorings and adding a lot of dif-
ferent code that we're forced to dig through.
Ideally, we could have a tool that helps us find these bugs before our code runs. That's what a static type-
checker like TypeScript does. Static types systems describe the shapes and behaviors of what our values
will be when we run our programs. A type-checker like TypeScript uses that information and tells us when
things might be going off the rails.
// @errors: 2349
const message = "hello!";
message();
Running that last sample with TypeScript will give us an error message before we run the code in the first
place.
Non-exception Failures
So far we've been discussing certain things like runtime errors - cases where the JavaScript runtime tells us
that it thinks something is nonsensical. Those cases come up because the ECMAScript specification has ex-
plicit instructions on how the language should behave when it runs into something unexpected.
141
typescript
For example, the specification says that trying to call something that isn't callable should throw an error.
Maybe that sounds like "obvious behavior", but you could imagine that accessing a property that doesn't ex-
ist on an object should throw an error too. Instead, JavaScript gives us different behavior and returns the
value undefined :
const user = {
name: "Daniel",
age: 26,
};
Ultimately, a static type system has to make the call over what code should be flagged as an error in its
system, even if it's "valid" JavaScript that won't immediately throw an error. In TypeScript, the following
code produces an error about location not being defined:
// @errors: 2339
const user = {
name: "Daniel",
age: 26,
};
user.location;
While sometimes that implies a trade-off in what you can express, the intent is to catch legitimate bugs in
our programs. And TypeScript catches a lot of legitimate bugs.
// @noErrors
const announcement = "Hello World!";
uncalled functions,
// @noUnusedLocals
// @errors: 2365
function flipCoin() {
// Meant to be Math.random()
return Math.random < 0.5;
}
// @errors: 2367
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
// ...
142
The Basics
The type-checker has information to check things like whether we're accessing the right properties on vari-
ables and other properties. Once it has that information, it can also start suggesting which properties you
might want to use.
That means TypeScript can be leveraged for editing code too, and the core type-checker can provide error
messages and code completion as you type in the editor. That's part of what people often refer to when
they talk about tooling in TypeScript.
// @noErrors
// @esModuleInterop
import express from "express";
const app = express();
app.listen(3000);
TypeScript takes tooling seriously, and that goes beyond completions and errors as you type. An editor that
supports TypeScript can deliver "quick fixes" to automatically fix errors, refactorings to easily re-organize
code, and useful navigation features for jumping to definitions of a variable, or finding all references to a
given variable. All of this is built on top of the type-checker and is fully cross-platform, so it's likely that
your favorite editor has TypeScript support available.
This installs the TypeScript Compiler tsc globally. You can use npx or similar tools if you'd prefer to
run tsc from a local node_modules package instead.
Now let's move to an empty folder and try writing our first TypeScript program: hello.ts :
143
typescript
Notice there are no frills here; this "hello world" program looks identical to what you'd write for a "hello
world" program in JavaScript. And now let's type-check it by running the command tsc which was installed
for us by the typescript package.
tsc hello.ts
Tada!
Wait, "tada" what exactly? We ran tsc and nothing happened! Well, there were no type errors, so we
didn't get any output in our console since there was nothing to report.
But check again - we got some file output instead. If we look in our current directory, we'll see a hello.js
file next to hello.ts . That's the output from our hello.ts file after tsc compiles or transforms it into a
plain JavaScript file. And if we check the contents, we'll see what TypeScript spits out after it processes a
.ts file:
In this case, there was very little for TypeScript to transform, so it looks identical to what we wrote. The
compiler tries to emit clean readable code that looks like something a person would write. While that's not
always so easy, TypeScript indents consistently, is mindful of when our code spans across different lines of
code, and tries to keep comments around.
// @noErrors
// This is an industrial-grade general-purpose greeter function:
function greet(person, date) {
console.log(`Hello ${person}, today is ${date}!`);
}
greet("Brendan");
If we run tsc hello.ts again, notice that we get an error on the command line!
TypeScript is telling us we forgot to pass an argument to the greet function, and rightfully so. So far we've
only written standard JavaScript, and yet type-checking was still able to find problems with our code.
Thanks TypeScript!
144
The Basics
To reiterate from earlier, type-checking code limits the sorts of programs you can run, and so there's a
tradeoff on what sorts of things a type-checker finds acceptable. Most of the time that's okay, but there are
scenarios where those checks get in the way. For example, imagine yourself migrating JavaScript code over
to TypeScript and introducing type-checking errors. Eventually you'll get around to cleaning things up for
the type-checker, but that original JavaScript code was already working! Why should converting it over to
TypeScript stop you from running it?
So TypeScript doesn't get in your way. Of course, over time, you may want to be a bit more defensive
against mistakes, and make TypeScript act a bit more strictly. In that case, you can use the noEmitOn‐
Error compiler option. Try changing your hello.ts file and running tsc with that flag:
Explicit Types
Up until now, we haven't told TypeScript what person or date are. Let's edit the code to tell TypeScript
that person is a string , and that date should be a Date object. We'll also use the toDateString()
method on date .
What we did was add type annotations on person and date to describe what types of values greet can
be called with. You can read that signature as " greet takes a person of type string , and a date of
type Date ".
With this, TypeScript can tell us about other cases where greet might have been called incorrectly. For
example...
// @errors: 2345
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", Date());
Perhaps surprisingly, calling Date() in JavaScript returns a string . On the other hand, constructing a
Date with new Date() actually gives us what we were expecting.
145
typescript
Keep in mind, we don't always have to write explicit type annotations. In many cases, TypeScript can even
just infer (or "figure out") the types for us even if we omit them.
Even though we didn't tell TypeScript that msg had the type string it was able to figure that out. That's a
feature, and it's best not to add annotations when the type system would end up inferring the same type
anyway.
Note: The message bubble inside the previous code sample is what your editor would show if you had
hovered over the word.
Erased Types
Let's take a look at what happens when we compile the above function greet with tsc to output
JavaScript:
// @showEmit
// @target: es5
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
More on that second point later, but let's now focus on that first point. Type annotations aren't part of
JavaScript (or ECMAScript to be pedantic), so there really aren't any browsers or other runtimes that can
just run TypeScript unmodified. That's why TypeScript needs a compiler in the first place - it needs some
way to strip out or transform any TypeScript-specific code so that you can run it. Most TypeScript-specific
code gets erased away, and likewise, here our type annotations were completely erased.
Remember: Type annotations never change the runtime behavior of your program.
Downleveling
One other difference from the above was that our template string was rewritten from
146
The Basics
to
Template strings are a feature from a version of ECMAScript called ECMAScript 2015 (a.k.a. ECMAScript 6,
ES2015, ES6, etc. - don't ask). TypeScript has the ability to rewrite code from newer versions of ECMAScript
to older ones such as ECMAScript 3 or ECMAScript 5 (a.k.a. ES3 and ES5). This process of moving from a
newer or "higher" version of ECMAScript down to an older or "lower" one is sometimes called downleveling.
By default TypeScript targets ES3, an extremely old version of ECMAScript. We could have chosen some-
thing a little bit more recent by using the target option. Running with --target es2015 changes
TypeScript to target ECMAScript 2015, meaning code should be able to run wherever ECMAScript 2015 is
supported. So running tsc --target es2015 hello.ts gives us the following output:
While the default target is ES3, the great majority of current browsers support ES2015. Most develop-
ers can therefore safely specify ES2015 or above as a target, unless compatibility with certain ancient
browsers is important.
Strictness
Different users come to TypeScript looking for different things in a type-checker. Some people are looking
for a more loose opt-in experience which can help validate only some parts of their program, and still have
decent tooling. This is the default experience with TypeScript, where types are optional, inference takes the
most lenient types, and there's no checking for potentially null / undefined values. Much like how tsc
emits in the face of errors, these defaults are put in place to stay out of your way. If you're migrating exist-
ing JavaScript, that might be a desirable first step.
In contrast, a lot of users prefer to have TypeScript validate as much as it can straight away, and that's why
the language provides strictness settings as well. These strictness settings turn static type-checking from a
switch (either your code is checked or not) into something closer to a dial. The further you turn this dial up,
the more TypeScript will check for you. This can require a little extra work, but generally speaking it pays
for itself in the long run, and enables more thorough checks and more accurate tooling. When possible, a
new codebase should always turn these strictness checks on.
TypeScript has several type-checking strictness flags that can be turned on or off, and all of our examples
will be written with all of them enabled unless otherwise stated. The strict flag in the CLI, or "strict":
true in a tsconfig.json toggles them all on simultaneously, but we can opt out of them individually. The
two biggest ones you should know about are noImplicitAny and strictNullChecks .
147
typescript
noImplicitAny
Recall that in some places, TypeScript doesn't try to infer types for us and instead falls back to the most le-
nient type: any . This isn't the worst thing that can happen - after all, falling back to any is just the plain
JavaScript experience anyway.
However, using any often defeats the purpose of using TypeScript in the first place. The more typed your
program is, the more validation and tooling you'll get, meaning you'll run into fewer bugs as you code.
Turning on the noImplicitAny flag will issue an error on any variables whose type is implicitly inferred as
any .
strictNullChecks
By default, values like null and undefined are assignable to any other type. This can make writing some
code easier, but forgetting to handle null and undefined is the cause of countless bugs in the world -
some consider it a billion dollar mistake! The strictNullChecks flag makes handling null and unde‐
fined more explicit, and spares us from worrying about whether we forgot to handle null and unde‐
fined .
Go to TOC
148
Classes
Background Reading:
Classes (MDN)
TypeScript offers full support for the class keyword introduced in ES2015.
As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you
to express relationships between classes and other types.
Class Members
Here's the most basic class - an empty one:
class Point {}
This class isn't very useful yet, so let's start adding some members.
Fields
A field declaration creates a public writeable property on a class:
// @strictPropertyInitialization: false
class Point {
x: number;
y: number;
}
As with other locations, the type annotation is optional, but will be an implicit any if not specified.
Fields can also have initializers; these will run automatically when the class is instantiated:
class Point {
x = 0;
y = 0;
}
Just like with const , let , and var , the initializer of a class property will be used to infer its type:
// @errors: 2322
class Point {
x = 0;
y = 0;
}
149
typescript
// ---cut---
const pt = new Point();
pt.x = "0";
--strictPropertyInitialization
The strictPropertyInitialization setting controls whether class fields need to be initialized in the
constructor.
// @errors: 2564
class BadGreeter {
name: string;
}
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
Note that the field needs to be initialized in the constructor itself. TypeScript does not analyze methods you
invoke from the constructor to detect initializations, because a derived class might override those methods
and fail to initialize the members.
If you intend to definitely initialize a field through means other than the constructor (for example, maybe an
external library is filling in part of your class for you), you can use the definite assignment assertion opera-
tor, ! :
class OKGreeter {
// Not initialized, but no error
name!: string;
}
readonly
Fields may be prefixed with the readonly modifier. This prevents assignments to the field outside of the
constructor.
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
}
}
const g = new Greeter();
g.name = "also not ok";
150
Classes
Constructors
Background Reading:
Constructor (MDN)
Class constructors are very similar to functions. You can add parameters with type annotations, default val-
ues, and overloads:
class Point {
x: number;
y: number;
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
There are just a few differences between class constructor signatures and function signatures:
Constructors can't have type parameters - these belong on the outer class declaration, which we'll learn
about later
Constructors can't have return type annotations - the class instance type is always what's returned
Super Calls
Just as in JavaScript, if you have a base class, you'll need to call super(); in your constructor body before
using any this. members:
// @errors: 17009
class Base {
k = 4;
}
151
typescript
Forgetting to call super is an easy mistake to make in JavaScript, but TypeScript will tell you when it's
necessary.
Methods
Background Reading:
Method definitions
A function property on a class is called a method. Methods can use all the same type annotations as func-
tions and constructors:
class Point {
x = 10;
y = 10;
Other than the standard type annotations, TypeScript doesn't add anything else new to methods.
Note that inside a method body, it is still mandatory to access fields and other methods via this. . An un-
qualified name in a method body will always refer to something in the enclosing scope:
// @errors: 2322
let x: number = 0;
class C {
x: string = "hello";
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
}
}
Getters / Setters
Classes can also have accessors:
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
152
Classes
Note that a field-backed get/set pair with no extra logic is very rarely useful in JavaScript. It's fine to
expose public fields if you don't need to add additional logic during the get/set operations.
Since TypeScript 4.3, it is possible to have accessors with different types for getting and setting.
class Thing {
_size = 0;
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
Index Signatures
Classes can declare index signatures; these work the same as Index Signatures for other object types:
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
Because the index signature type needs to also capture the types of methods, it's not easy to usefully use
these types. Generally it's better to store indexed data in another place instead of on the class instance
itself.
Class Heritage
Like other languages with object-oriented features, classes in JavaScript can inherit from base classes.
153
typescript
implements Clauses
You can use an implements clause to check that a class satisfies a particular interface . An error will be
issued if a class fails to correctly implement it:
// @errors: 2420
interface Pingable {
ping(): void;
}
Cautions
It's important to understand that an implements clause is only a check that the class can be treated as the
interface type. It doesn't change the type of the class or its methods at all. A common source of error is to
assume that an implements clause will change the class type - it doesn't!
// @errors: 7006
interface Checkable {
check(name: string): boolean;
}
In this example, we perhaps expected that s 's type would be influenced by the name: string parameter
of check . It is not - implements clauses don't change how the class body is checked or its type inferred.
Similarly, implementing an interface with an optional property doesn't create that property:
// @errors: 2339
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
154
Classes
}
const c = new C();
c.y = 10;
extends Clauses
Background Reading:
extends keyword (MDN)
Classes may extend from a base class. A derived class has all the properties and methods of its base
class, and also define additional members.
class Animal {
move() {
console.log("Moving along!");
}
}
Overriding Methods
Background Reading:
super keyword (MDN)
A derived class can also override a base class field or property. You can use the super. syntax to access
base class methods. Note that because JavaScript classes are a simple lookup object, there is no notion of a
"super field".
TypeScript enforces that a derived class is always a subtype of its base class.
class Base {
greet() {
console.log("Hello, world!");
}
}
155
typescript
It's important that a derived class follow its base class contract. Remember that it's very common (and al-
ways legal!) to refer to a derived class instance through a base class reference:
class Base {
greet() {
console.log("Hello, world!");
}
}
declare const d: Base;
// ---cut---
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
// @errors: 2416
class Base {
greet() {
console.log("Hello, world!");
}
}
If we compiled this code despite the error, this sample would then crash:
156
Classes
When target >= ES2022 or useDefineForClassFields is true , class fields are initialized after the par-
ent class constructor completes, overwriting any value set by the parent class. This can be a problem when
you only want to re-declare a more accurate type for an inherited field. To handle these cases, you can
write declare to indicate to TypeScript that there should be no runtime effect for this field declaration.
interface Animal {
dateOfBirth: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
Initialization Order
The order that JavaScript classes initialize can be surprising in some cases. Let's consider this code:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
157
typescript
This means that the base class constructor saw its own value for name during its own constructor, because
the derived class field initializations hadn't run yet.
Note: If you don't plan to inherit from built-in types like Array , Error , Map , etc. or your compila-
tion target is explicitly set to ES6 / ES2015 or above, you may skip this section
In ES2015, constructors which return an object implicitly substitute the value of this for any callers of
super(...) . It is necessary for generated constructor code to capture any potential return value of
super(...) and replace it with this .
As a result, subclassing Error , Array , and others may no longer work as expected. This is due to the fact
that constructor functions for Error , Array , and the like use ECMAScript 6's new.target to adjust the
prototype chain; however, there is no way to ensure a value for new.target when invoking a constructor
in ECMAScript 5. Other downlevel compilers generally have the same limitation by default.
methods may be undefined on objects returned by constructing these subclasses, so calling sayHello
will result in an error.
instanceof will be broken between instances of the subclass and their instances, so (new
MsgError()) instanceof MsgError will return false .
As a recommendation, you can manually adjust the prototype immediately after any super(...) calls.
sayHello() {
return "hello " + this.message;
}
}
158
Classes
However, any subclass of MsgError will have to manually set the prototype as well. For runtimes that don't
support Object.setPrototypeOf , you may instead be able to use __proto__ .
Unfortunately, these workarounds will not work on Internet Explorer 10 and prior. One can manually copy
methods from the prototype onto the instance itself (i.e. MsgError.prototype onto this ), but the proto-
type chain itself cannot be fixed.
Member Visibility
You can use TypeScript to control whether certain methods or properties are visible to code outside the
class.
public
The default visibility of class members is public . A public member can be accessed anywhere:
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
Because public is already the default visibility modifier, you don't ever need to write it on a class member,
but might choose to do so for style/readability reasons.
protected
protected members are only visible to subclasses of the class they're declared in.
// @errors: 2445
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
159
typescript
Derived classes need to follow their base class contracts, but may choose to expose a subtype of base class
with more capabilities. This includes making protected members public :
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
Note that Derived was already able to freely read and write m , so this doesn't meaningfully alter the "se-
curity" of this situation. The main thing to note here is that in the derived class, we need to be careful to re-
peat the protected modifier if this exposure isn't intentional.
Different OOP languages disagree about whether it's legal to access a protected member through a base
class reference:
// @errors: 2446
class Base {
protected x: number = 1;
}
class Derived1 extends Base {
protected x: number = 5;
}
class Derived2 extends Base {
f1(other: Derived2) {
other.x = 10;
}
f2(other: Base) {
other.x = 10;
}
}
Java, for example, considers this to be legal. On the other hand, C# and C++ chose that this code should
be illegal.
TypeScript sides with C# and C++ here, because accessing x in Derived2 should only be legal from
Derived2 's subclasses, and Derived1 isn't one of them. Moreover, if accessing x through a Derived1
reference is illegal (which it certainly should be!), then accessing it through a base class reference should
never improve the situation.
See also Why Can’t I Access A Protected Member From A Derived Class? which explains more of C#'s
reasoning.
private
private is like protected , but doesn't allow access to the member even from subclasses:
160
Classes
// @errors: 2341
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
// @errors: 2341
class Base {
private x = 0;
}
// ---cut---
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
}
}
Because private members aren't visible to derived classes, a derived class can't increase its visibility:
// @errors: 2415
class Base {
private x = 0;
}
class Derived extends Base {
x = 1;
}
Different OOP languages disagree about whether different instances of the same class may access each oth-
ers' private members. While languages like Java, C#, C++, Swift, and PHP allow this, Ruby does not.
class A {
private x = 10;
public sameAs(other: A) {
// No error
return other.x === this.x;
}
}
Caveats
Like other aspects of TypeScript's type system, private and protected are only enforced during type
checking.
This means that JavaScript runtime constructs like in or simple property lookup can still access a pri‐
vate or protected member:
class MySafe {
private secretKey = 12345;
}
161
typescript
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);
private also allows access using bracket notation during type checking. This makes private -declared
fields potentially easier to access for things like unit tests, with the drawback that these fields are soft pri-
vate and don't strictly enforce privacy.
// @errors: 2341
class MySafe {
private secretKey = 12345;
}
// OK
console.log(s["secretKey"]);
Unlike TypeScripts's private , JavaScript's private fields ( # ) remain private after compilation and do not
provide the previously mentioned escape hatches like bracket notation access, making them hard private.
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() {}
}
// @target: esnext
// @showEmit
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() {}
}
// @target: es2015
// @showEmit
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() {}
}
If you need to protect values in your class from malicious actors, you should use mechanisms that offer
hard runtime privacy, such as closures, WeakMaps, or private fields. Note that these added privacy checks
during runtime could affect performance.
162
Classes
Static Members
Background Reading:
Static Members (MDN)
Classes may have static members. These members aren't associated with a particular instance of the
class. They can be accessed through the class constructor object itself:
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
Static members can also use the same public , protected , and private visibility modifiers:
// @errors: 2341
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
// @errors: 2699
class S {
static name = "S!";
}
163
typescript
Those constructs only exist because those languages force all data and functions to be inside a class; be-
cause that restriction doesn't exist in TypeScript, there's no need for them. A class with only a single in-
stance is typically just represented as a normal object in JavaScript/TypeScript.
For example, we don't need a "static class" syntax in TypeScript because a regular object (or even top-level
function) will do the job just as well:
// Preferred (alternative 1)
function doSomething() {}
// Preferred (alternative 2)
const MyHelperObject = {
dosomething() {},
};
get count() {
return Foo.#count;
}
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
Generic Classes
Classes, much like interfaces, can be generic. When a generic class is instantiated with new , its type para-
meters are inferred the same way as in a function call:
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
164
Classes
Classes can use generic constraints and defaults the same way as interfaces.
// @errors: 2302
class Box<Type> {
static defaultValue: Type;
}
Remember that types are always fully erased! At runtime, there's only one Box.defaultValue property
slot. This means that setting Box<string>.defaultValue (if that were possible) would also change
Box<number>.defaultValue - not good. The static members of a generic class can never refer to the
class's type parameters.
Background Reading:
this keyword (MDN)
It's important to remember that TypeScript doesn't change the runtime behavior of JavaScript, and that
JavaScript is somewhat famous for having some peculiar runtime behaviors.
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
Long story short, by default, the value of this inside a function depends on how the function was called.
In this example, because the function was called through the obj reference, its value of this was obj
rather than the class instance.
This is rarely what you want to happen! TypeScript provides some ways to mitigate or prevent this kind of
error.
165
typescript
Arrow Functions
Background Reading:
Arrow functions (MDN)
If you have a function that will often be called in a way that loses its this context, it can make sense to
use an arrow function property instead of a method definition:
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());
The this value is guaranteed to be correct at runtime, even for code not checked with TypeScript
This will use more memory, because each class instance will have its own copy of each function defined
this way
You can't use super.getName in a derived class, because there's no entry in the prototype chain to
fetch the base class method from
this parameters
In a method or function definition, an initial parameter named this has special meaning in TypeScript.
These parameters are erased during compilation:
// JavaScript output
function fn(x) {
/* ... */
}
TypeScript checks that calling a function with a this parameter is done so with a correct context. Instead
of using an arrow function, we can add a this parameter to method definitions to statically enforce that
the method is called correctly:
// @errors: 2684
class MyClass {
name = "MyClass";
166
Classes
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
This method makes the opposite trade-offs of the arrow function approach:
JavaScript callers might still use the class method incorrectly without realizing it
Only one function per class definition gets allocated, rather than one per class instance
Base method definitions can still be called via super .
this Types
In classes, a special type called this refers dynamically to the type of the current class. Let's see how this
is useful:
class Box {
contents: string = "";
set(value: string) {
// ^?
this.contents = value;
return this;
}
}
Here, TypeScript inferred the return type of set to be this , rather than Box . Now let's make a subclass
of Box :
class Box {
contents: string = "";
set(value: string) {
this.contents = value;
return this;
}
}
// ---cut---
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
167
typescript
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
This is different from writing other: Box -- if you have a derived class, its sameAs method will now only
accept other instances of that same derived class:
// @errors: 2345
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
// @strictPropertyInitialization: false
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
interface Networked {
host: string;
}
168
Classes
if (fso.isFile()) {
fso.content;
// ^?
} else if (fso.isDirectory()) {
fso.children;
// ^?
} else if (fso.isNetworked()) {
fso.host;
// ^?
}
A common use-case for a this-based type guard is to allow for lazy validation of a particular field. For exam-
ple, this case removes an undefined from the value held inside box when hasValue has been verified to
be true:
class Box<T> {
value?: T;
box.value;
// ^?
if (box.hasValue()) {
box.value;
// ^?
}
Parameter Properties
TypeScript offers special syntax for turning a constructor parameter into a class property with the same
name and value. These are called parameter properties and are created by prefixing a constructor argument
with one of the visibility modifiers public , private , protected , or readonly . The resulting field gets
those modifier(s):
// @errors: 2341
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
console.log(a.x);
// ^?
console.log(a.z);
169
typescript
Class Expressions
Background Reading:
Class expressions (MDN)
Class expressions are very similar to class declarations. The only real difference is that class expressions
don't need a name, though we can refer to them via whatever identifier they ended up bound to:
An abstract method or abstract field is one that hasn't had an implementation provided. These members
must exist inside an abstract class, which cannot be directly instantiated.
The role of abstract classes is to serve as a base class for subclasses which do implement all the abstract
members. When a class doesn't have any abstract members, it is said to be concrete.
// @errors: 2511
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
We can't instantiate Base with new because it's abstract. Instead, we need to make a derived class and
implement the abstract members:
170
Classes
}
}
Notice that if we forget to implement the base class's abstract members, we'll get an error:
// @errors: 2515
abstract class Base {
abstract getName(): string;
printName() {}
}
// ---cut---
class Derived extends Base {
// forgot to do anything
}
// @errors: 2511
abstract class Base {
abstract getName(): string;
printName() {}
}
class Derived extends Base {
getName() {
return "";
}
}
// ---cut---
function greet(ctor: typeof Base) {
const instance = new ctor();
instance.printName();
}
TypeScript is correctly telling you that you're trying to instantiate an abstract class. After all, given the defi-
nition of greet , it's perfectly legal to write this code, which would end up constructing an abstract class:
Instead, you want to write a function that accepts something with a construct signature:
// @errors: 2345
abstract class Base {
abstract getName(): string;
printName() {}
}
class Derived extends Base {
171
typescript
getName() {
return "";
}
}
// ---cut---
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base);
Now TypeScript correctly tells you about which class constructor functions can be invoked - Derived can
because it's concrete, but Base cannot.
For example, these two classes can be used in place of each other because they're identical:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();
Similarly, subtype relationships between classes exist even if there's no explicit inheritance:
// @strict: false
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();
This sounds straightforward, but there are a few cases that seem stranger than others.
Empty classes have no members. In a structural type system, a type with no members is generally a super-
type of anything else. So if you write an empty class (don't!), anything can be used in place of it:
172
Classes
class Empty {}
// All OK!
fn(window);
fn({});
fn(fn);
Go to TOC
173
typescript
In this chapter, we'll cover some of the most common types of values you'll find in JavaScript code, and ex-
plain the corresponding ways to describe those types in TypeScript. This isn't an exhaustive list, and future
chapters will describe more ways to name and use other types.
Types can also appear in many more places than just type annotations. As we learn about the types them-
selves, we'll also learn about the places where we can refer to these types to form new constructs.
We'll start by reviewing the most basic and common types you might encounter when writing JavaScript or
TypeScript code. These will later form the core building blocks of more complex types.
The type names String , Number , and Boolean (starting with capital letters) are legal, but refer to
some special built-in types that will very rarely appear in your code. Always use string , number , or
boolean for types.
Arrays
To specify the type of an array like [1, 2, 3] , you can use the syntax number[] ; this syntax works for
any type (e.g. string[] is an array of strings, and so on). You may also see this written as
Array<number> , which means the same thing. We'll learn more about the syntax T<U> when we cover
generics.
any
TypeScript also has a special type, any , that you can use whenever you don't want a particular value to
cause typechecking errors.
174
Everyday Types
When a value is of type any , you can access any properties of it (which will in turn be of type any ), call it
like a function, assign it to (or from) a value of any type, or pretty much anything else that's syntactically
legal:
The any type is useful when you don't want to write out a long type just to convince TypeScript that a par-
ticular line of code is okay.
noImplicitAny
When you don't specify a type, and TypeScript can't infer it from context, the compiler will typically default
to any .
You usually want to avoid this, though, because any isn't type-checked. Use the compiler flag noImplici‐
tAny to flag any implicit any as an error.
TypeScript doesn't use "types on the left"-style declarations like int x = 0; Type annotations will al-
ways go after the thing being typed.
In most cases, though, this isn't needed. Wherever possible, TypeScript tries to automatically infer the
types in your code. For example, the type of a variable is inferred based on the type of its initializer:
For the most part you don't need to explicitly learn the rules of inference. If you're starting out, try using
fewer type annotations than you think - you might be surprised how few you need for TypeScript to fully un-
derstand what's going on.
175
typescript
Functions
Functions are the primary means of passing data around in JavaScript. TypeScript allows you to specify the
types of both the input and output values of functions.
When a parameter has a type annotation, arguments to that function will be checked:
// @errors: 2345
declare function greet(name: string): void;
// ---cut---
// Would be a runtime error if executed!
greet(42);
Even if you don't have type annotations on your parameters, TypeScript will still check that you
passed the right number of arguments.
Much like variable type annotations, you usually don't need a return type annotation because TypeScript will
infer the function's return type based on its return statements. The type annotation in the above example
doesn't change anything. Some codebases will explicitly specify a return type for documentation purposes,
to prevent accidental changes, or just for personal preference.
Anonymous Functions
Anonymous functions are a little bit different from function declarations. When a function appears in a place
where TypeScript can determine how it's going to be called, the parameters of that function are automati-
cally given types.
Here's an example:
176
Everyday Types
// @errors: 2551
// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];
Even though the parameter s didn't have a type annotation, TypeScript used the types of the forEach
function, along with the inferred type of the array, to determine the type s will have.
This process is called contextual typing because the context that the function occurred within informs what
type it should have.
Similar to the inference rules, you don't need to explicitly learn how this happens, but understanding that it
does happen can help you notice when type annotations aren't needed. Later, we'll see more examples of
how the context that a value occurs in can affect its type.
Object Types
Apart from primitives, the most common sort of type you'll encounter is an object type. This refers to any
JavaScript value with properties, which is almost all of them! To define an object type, we simply list its
properties and their types.
Here, we annotated the parameter with a type with two properties - x and y - which are both of type
number . You can use , or ; to separate the properties, and the last separator is optional either way.
The type part of each property is also optional. If you don't specify a type, it will be assumed to be any .
Optional Properties
Object types can also specify that some or all of their properties are optional. To do this, add a ? after the
property name:
177
typescript
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
In JavaScript, if you access a property that doesn't exist, you'll get the value undefined rather than a run-
time error. Because of this, when you read from an optional property, you'll have to check for undefined
before using it.
// @errors: 2532
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
Union Types
TypeScript's type system allows you to build new types out of existing ones using a large variety of opera-
tors. Now that we know how to write a few types, it's time to start combining them in interesting ways.
// @errors: 2345
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
TypeScript will only allow an operation if it is valid for every member of the union. For example, if you have
the union string | number , you can't use methods that are only available on string :
178
Everyday Types
// @errors: 2339
function printId(id: number | string) {
console.log(id.toUpperCase());
}
The solution is to narrow the union with code, the same as you would in JavaScript without type
annotations. Narrowing occurs when TypeScript can deduce a more specific type for a value based on the
structure of the code.
For example, TypeScript knows that only a string value will have a typeof value "string" :
Notice that in the else branch, we don't need to do anything special - if x wasn't a string[] , then it
must have been a string .
Sometimes you'll have a union where all the members have something in common. For example, both ar-
rays and strings have a slice method. If every member in a union has a property in common, you can use
that property without narrowing:
It might be confusing that a union of types appears to have the intersection of those types'
properties. This is not an accident - the name union comes from type theory. The union number | st
ring is composed by taking the union of the values from each type. Notice that given two sets with
corresponding facts about each set, only the intersection of those facts applies to the union of the sets
179
typescript
themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish
speakers wearing hats, after combining those rooms, the only thing we know about every person is
that they must be wearing a hat.
Type Aliases
We've been using object types and union types by writing them directly in type annotations. This is conve-
nient, but it's common to want to use the same type more than once and refer to it by a single name.
A type alias is exactly that - a name for any type. The syntax for a type alias is:
type Point = {
x: number;
y: number;
};
You can actually use a type alias to give a name to any type at all, not just an object type. For example, a
type alias can name a union type:
Note that aliases are only aliases - you cannot use type aliases to create different/distinct "versions" of the
same type. When you use the alias, it's exactly as if you had written the aliased type. In other words, this
code might look illegal, but is OK according to TypeScript because both types are aliases for the same type:
Interfaces
An interface declaration is another way to name an object type:
180
Everyday Types
interface Point {
x: number;
y: number;
}
Just like when we used a type alias above, the example works just as if we had used an anonymous object
type. TypeScript is only concerned with the structure of the value we passed to printCoord - it only cares
that it has the expected properties. Being concerned only with the structure and capabilities of types is why
we call TypeScript a structurally typed type system.
Interface Type
Adding new fields to an existing interface A type cannot be changed after being created
181
typescript
You'll learn more about these concepts in later chapters, so don't worry if you don't understand all of these
right away.
Prior to TypeScript version 4.2, type alias names may appear in error messages, sometimes in place of
the equivalent anonymous type (which may or may not be desirable). Interfaces will always be named in
error messages.
Type aliases may not participate in declaration merging, but interfaces can.
Interfaces may only be used to declare the shapes of objects, not rename primitives.
Interface names will always appear in their original form in error messages, but only when they are used
by name.
For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs
something to be the other kind of declaration. If you would like a heuristic, use interface until you need
to use features from type .
Type Assertions
Sometimes you will have information about the type of a value that TypeScript can't know about.
For example, if you're using document.getElementById , TypeScript only knows that this will return some
kind of HTMLElement , but you might know that your page will always have an HTMLCanvasElement with a
given ID.
In this situation, you can use a type assertion to specify a more specific type:
Like a type annotation, type assertions are removed by the compiler and won't affect the runtime behavior
of your code.
182
Everyday Types
You can also use the angle-bracket syntax (except if the code is in a .tsx file), which is equivalent:
Reminder: Because type assertions are removed at compile-time, there is no runtime checking associ-
ated with a type assertion. There won't be an exception or null generated if the type assertion is
wrong.
TypeScript only allows type assertions which convert to a more specific or less specific version of a type.
This rule prevents "impossible" coercions like:
// @errors: 2352
const x = "hello" as number;
Sometimes this rule can be too conservative and will disallow more complex coercions that might be valid.
If this happens, you can use two assertions, first to any (or unknown , which we'll introduce later), then to
the desired type:
Literal Types
In addition to the general types string and number , we can refer to specific strings and numbers in type
positions.
One way to think about this is to consider how JavaScript comes with different ways to declare a variable.
Both var and let allow for changing what is held inside the variable, and const does not. This is reflect-
ed in how TypeScript creates types for literals.
// @errors: 2322
let x: "hello" = "hello";
// OK
183
typescript
x = "hello";
// ...
x = "howdy";
It's not much use to have a variable that can only have one value!
But by combining literals into unions, you can express a much more useful concept - for example, functions
that only accept a certain set of known values:
// @errors: 2345
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// @errors: 2345
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
There's one more kind of literal type: boolean literals. There are only two boolean literal types, and as you
might guess, they are the types true and false . The type boolean itself is actually just an alias for the
union true | false .
Literal Inference
When you initialize a variable with an object, TypeScript assumes that the properties of that object might
change values later. For example, if you wrote code like this:
TypeScript doesn't assume the assignment of 1 to a field which previously had 0 is an error. Another way
of saying this is that obj.counter must have the type number , not 0 , because types are used to deter-
mine both reading and writing behavior.
184
Everyday Types
// @errors: 2345
declare function handleRequest(url: string, method: "GET" | "POST"): void;
// ---cut---
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
In the above example req.method is inferred to be string , not "GET" . Because code can be evaluated
between the creation of req and the call of handleRequest which could assign a new string like "GUESS"
to req.method , TypeScript considers this code to have an error.
1. You can change the inference by adding a type assertion in either location:
Change 1 means "I intend for req.method to always have the literal type "GET" ", preventing the possi-
ble assignment of "GUESS" to that field after. Change 2 means "I know for other reasons that re‐
q.method has the value "GET" ".
2. You can use as const to convert the entire object to be type literals:
The as const suffix acts like const but for the type system, ensuring that all properties are assigned the
literal type instead of a more general version like string or number .
TypeScript has two corresponding types by the same names. How these types behave depends on whether
you have the strictNullChecks option on.
strictNullChecks off
With strictNullChecks off, values that might be null or undefined can still be accessed normally, and
the values null and undefined can be assigned to a property of any type. This is similar to how lan-
guages without null checks (e.g. C#, Java) behave. The lack of checking for these values tends to be a ma-
jor source of bugs; we always recommend people turn strictNullChecks on if it's practical to do so in
their codebase.
185
typescript
strictNullChecks on
With strictNullChecks on, when a value is null or undefined , you will need to test for those values
before using methods or properties on that value. Just like checking for undefined before using an option-
al property, we can use narrowing to check for values that might be null :
Just like other type assertions, this doesn't change the runtime behavior of your code, so it's important to
only use ! when you know that the value can't be null or undefined .
Enums
Enums are a feature added to JavaScript by TypeScript which allows for describing a value which could be
one of a set of possible named constants. Unlike most TypeScript features, this is not a type-level addition
to JavaScript but something added to the language and runtime. Because of this, it's a feature which you
should know exists, but maybe hold off on using unless you are sure. You can read more about enums in
the Enum reference page.
bigint
From ES2020 onwards, there is a primitive in JavaScript used for very large integers, BigInt :
// @target: es2020
186
Everyday Types
You can learn more about BigInt in the TypeScript 3.2 release notes.
symbol
There is a primitive in JavaScript used to create a globally unique reference via the function Symbol() :
// @errors: 2367
const firstName = Symbol("name");
const secondName = Symbol("name");
Go to TOC
187
typescript
JavaScript has a long history of different ways to handle modularizing code. TypeScript having been around
since 2012, has implemented support for a lot of these formats, but over time the community and the
JavaScript specification has converged on a format called ES Modules (or ES6 modules). You might know it
as the import / export syntax.
ES Modules was added to the JavaScript spec in 2015, and by 2020 had broad support in most web
browsers and JavaScript runtimes.
For focus, the handbook will cover both ES Modules and its popular pre-cursor CommonJS module.exports
= syntax, and you can find information about the other module patterns in the reference section under
Modules.
Conversely, a file without any top-level import or export declarations is treated as a script whose contents
are available in the global scope (and therefore to modules as well).
Modules are executed within their own scope, not in the global scope. This means that variables, functions,
classes, etc. declared in a module are not visible outside the module unless they are explicitly exported us-
ing one of the export forms. Conversely, to consume a variable, function, class, interface, etc. exported
from a different module, it has to be imported using one of the import forms.
Non-modules
Before we start, it's important to understand what TypeScript considers a module. The JavaScript specifica-
tion declares that any JavaScript files without an export or top-level await should be considered a script
and not a module.
Inside a script file variables and types are declared to be in the shared global scope, and it's assumed that
you'll either use the outFile compiler option to join multiple input files into one output file, or use multiple
<script> tags in your HTML to load these files (in the correct order!).
If you have a file that doesn't currently have any import s or export s, but you want to be treated as a
module, add the line:
export {};
which will change the file to be a module exporting nothing. This syntax works regardless of your module
target.
188
Modules
Modules in TypeScript
Additional Reading:
Impatient JS (Modules)
MDN: JavaScript Modules
There are three main things to consider when writing module-based code in TypeScript:
ES Module Syntax
A file can declare a main export via export default :
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
// @filename: index.ts
// ---cut---
import helloWorld from "./hello.js";
helloWorld();
In addition to the default export, you can have more than one export of variables and functions via the ex‐
port by omitting default :
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
189
typescript
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
export class RandomNumberGenerator {}
export function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
// @filename: app.ts
// ---cut---
import { pi, phi, absolute } from "./maths.js";
console.log(pi);
const absPhi = absolute(phi);
// ^?
// @filename: maths.ts
export var pi = 3.14;
// @filename: app.ts
// ---cut---
import { pi as π } from "./maths.js";
console.log(π);
// ^?
You can mix and match the above syntax into a single import :
// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
// @filename: app.ts
import RandomNumberGenerator, { pi as π } from "./maths.js";
RandomNumberGenerator;
// ^?
console.log(π);
// ^?
You can take all of the exported objects and put them into a single namespace using * as name :
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
190
Modules
console.log(math.pi);
const positivePhi = math.absolute(math.phi);
// ^?
You can import a file and not include any variables into your current module via import "./file" :
// @filename: maths.ts
export var pi = 3.14;
// ---cut---
// @filename: app.ts
import "./maths.js";
console.log("3.14");
In this case, the import does nothing. However, all of the code in maths.ts was evaluated, which could
trigger side-effects which affect other objects.
Types can be exported and imported using the same syntax as JavaScript values:
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;
TypeScript has extended the import syntax with two concepts for declaring an import of a type:
import type
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
// @filename: app.ts
// @errors: 1361
import type { createCatName } from "./animal.js";
const name = createCatName();
TypeScript 4.5 also allows for individual imports to be prefixed with type to indicate that the imported ref-
erence is a type:
191
typescript
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// ---cut---
// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
Together these allow a non-TypeScript transpiler like Babel, swc or esbuild to know what imports can be
safely removed.
TypeScript has ES Module syntax which directly correlates to a CommonJS and AMD require . Imports us-
ing ES Module are for most cases the same as the require from those environments, but this syntax en-
sures you have a 1 to 1 match in your TypeScript file with the CommonJS output:
You can learn more about this syntax in the modules reference page.
CommonJS Syntax
CommonJS is the format which most modules on npm are delivered in. Even if you are writing using the ES
Modules syntax above, having a brief understanding of how CommonJS syntax works will help you debug
easier.
Exporting
Identifiers are exported via setting the exports property on a global called module .
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
192
Modules
// @module: commonjs
// @filename: maths.ts
/// <reference types="node" />
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
// @filename: index.ts
// ---cut---
const maths = require("maths");
maths.pi;
// ^?
// @module: commonjs
// @filename: maths.ts
/// <reference types="node" />
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
// @filename: index.ts
// ---cut---
const { squareTwo } = require("maths");
squareTwo;
// ^?
TypeScript includes two resolution strategies: Classic and Node. Classic, the default when the compiler op-
tion module is not commonjs , is included for backwards compatibility. The Node strategy replicates how
Node.js works in CommonJS mode, with additional checks for .ts and .d.ts .
193
typescript
There are many TSConfig flags which influence the module strategy within TypeScript: moduleResolution ,
baseUrl , paths , rootDirs .
For the full details on how these strategies work, you can consult the Module Resolution.
target which determines which JS features are downleveled (converted to run in older JavaScript run-
times) and which are left intact
module which determines what code is used for modules to interact with each other
Which target you use is determined by the features available in the JavaScript runtime you expect to run
the TypeScript code in. That could be: the oldest web browser you support, the lowest version of Node.js
you expect to run on or could come from unique constraints from your runtime - like Electron for example.
All communication between modules happens via a module loader, the compiler option module determines
which one is used. At runtime the module loader is responsible for locating and executing all dependencies
of a module before executing it.
For example, here is a TypeScript file using ES Modules syntax, showcasing a few different options for mod‐
ule :
// @filename: constants.ts
export const valueOfPi = 3.142;
// @filename: index.ts
// ---cut---
import { valueOfPi } from "./constants.js";
ES2020
// @showEmit
// @module: es2020
// @noErrors
import { valueOfPi } from "./constants.js";
CommonJS
// @showEmit
// @module: commonjs
// @noErrors
import { valueOfPi } from "./constants.js";
194
Modules
UMD
// @showEmit
// @module: umd
// @noErrors
import { valueOfPi } from "./constants.js";
You can see all of the available options and what their emitted JavaScript code looks like in the TSConfig
Reference for module .
TypeScript namespaces
TypeScript has its own module format called namespaces which pre-dates the ES Modules standard. This
syntax has a lot of useful features for creating complex definition files, and still sees active use in
DefinitelyTyped. While not deprecated, the majority of the features in namespaces exist in ES Modules and
we recommend you use that to align with JavaScript's direction. You can learn more about namespaces in
the namespaces reference page.
Go to TOC
195
typescript
Functions are the basic building block of any application, whether they're local functions, imported from an-
other module, or methods on a class. They're also values, and just like other values, TypeScript has many
ways to describe how functions can be called. Let's learn about how to write types that describe functions.
greeter(printToConsole);
The syntax (a: string) => void means "a function with one parameter, named a , of type string, that
doesn't have a return value". Just like with function declarations, if a parameter type isn't specified, it's im-
plicitly any .
Note that the parameter name is required. The function type (string) => void means "a function
with a parameter named string of type any "!
Call Signatures
In JavaScript, functions can have properties in addition to being callable. However, the function type expres-
sion syntax doesn't allow for declaring properties. If we want to describe something callable with properties,
we can write a call signature in an object type:
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
196
More on Functions
Note that the syntax is slightly different compared to a function type expression - use : between the para-
meter list and the return type rather than => .
Construct Signatures
JavaScript functions can also be invoked with the new operator. TypeScript refers to these as constructors
because they usually create a new object. You can write a construct signature by adding the new keyword
in front of a call signature:
Some objects, like JavaScript's Date object, can be called with or without new . You can combine call and
construct signatures in the same type arbitrarily:
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
Generic Functions
It's common to write a function where the types of the input relate to the type of the output, or where the
types of two inputs are related in some way. Let's consider for a moment a function that returns the first
element of an array:
This function does its job, but unfortunately has the return type any . It'd be better if the function returned
the type of the array element.
In TypeScript, generics are used when we want to describe a correspondence between two values. We do
this by declaring a type parameter in the function signature:
By adding a type parameter Type to this function and using it in two places, we've created a link between
the input of the function (the array) and the output (the return value). Now when we call it, a more specific
type comes out:
197
typescript
Inference
Note that we didn't have to specify Type in this sample. The type was inferred - chosen automatically - by
TypeScript.
We can use multiple type parameters as well. For example, a standalone version of map would look like
this:
// prettier-ignore
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[]
{
return arr.map(func);
}
Note that in this example, TypeScript could infer both the type of the Input type parameter (from the giv-
en string array), as well as the Output type parameter based on the return value of the function expres-
sion ( number ).
Constraints
We've written some generic functions that can work on any kind of value. Sometimes we want to relate two
values, but can only operate on a certain subset of values. In this case, we can use a constraint to limit the
kinds of types that a type parameter can accept.
Let's write a function that returns the longer of two values. To do this, we need a length property that's a
number. We constrain the type parameter to that type by writing an extends clause:
198
More on Functions
There are a few interesting things to note in this example. We allowed TypeScript to infer the return type of
longest . Return type inference also works on generic functions.
Because we constrained Type to { length: number } , we were allowed to access the .length property
of the a and b parameters. Without the type constraint, we wouldn't be able to access those properties
because the values might have been some other type without a length property.
The types of longerArray and longerString were inferred based on the arguments. Remember, generics
are all about relating two or more values with the same type!
Finally, just as we'd like, the call to longest(10, 100) is rejected because the number type doesn't have
a .length property.
// @errors: 2322
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
}
}
It might look like this function is OK - Type is constrained to { length: number } , and the function ei-
ther returns Type or a value matching that constraint. The problem is that the function promises to return
the same kind of object as was passed in, not just some object matching the constraint. If this code were
legal, you could write code that definitely wouldn't work:
199
typescript
// @errors: 2322
declare function combine<Type>(arr1: Type[], arr2: Type[]): Type[];
// ---cut---
const arr = combine([1, 2, 3], ["hello"]);
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
These might seem identical at first glance, but firstElement1 is a much better way to write this function.
Its inferred return type is Type , but firstElement2 's inferred return type is any because TypeScript has
to resolve the arr[0] expression using the constraint type, rather than "waiting" to resolve the element
during a call.
Rule: When possible, use the type parameter itself rather than constraining it
200
More on Functions
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
We've created a type parameter Func that doesn't relate two values. That's always a red flag, because it
means callers wanting to specify type arguments have to manually specify an extra type argument for no
reason. Func doesn't do anything but make the function harder to read and reason about!
greet("world");
Remember, type parameters are for relating the types of multiple values. If a type parameter is only used
once in the function signature, it's not relating anything.
Rule: If a type parameter only appears in one location, strongly reconsider if you actually need it
Optional Parameters
Functions in JavaScript often take a variable number of arguments. For example, the toFixed method of
number takes an optional digit count:
201
typescript
Although the parameter is specified as type number , the x parameter will actually have the type number
| undefined because unspecified parameters in JavaScript get the value undefined .
Now in the body of f , x will have type number because any undefined argument will be replaced with
10 . Note that when a parameter is optional, callers can always pass undefined , as this simply simulates a
"missing" argument:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
What people usually intend when writing index? as an optional parameter is that they want both of these
calls to be legal:
// @errors: 2532
declare function myForEach(
arr: any[],
callback: (arg: any, index?: number) => void
): void;
// ---cut---
myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));
What this actually means is that callback might get invoked with one argument. In other words, the func-
tion definition says that the implementation might look like this:
202
More on Functions
// @errors: 2532
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
// I don't feel like providing the index today
callback(arr[i]);
}
}
In turn, TypeScript will enforce this meaning and issue errors that aren't really possible:
// @errors: 2532
declare function myForEach(
arr: any[],
callback: (arg: any, index?: number) => void
): void;
// ---cut---
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed());
});
In JavaScript, if you call a function with more arguments than there are parameters, the extra arguments
are simply ignored. TypeScript behaves the same way. Functions with fewer parameters (of the same types)
can always take the place of functions with more parameters.
When writing a function type for a callback, never write an optional parameter unless you intend to
call the function without passing that argument
Function Overloads
Some JavaScript functions can be called in a variety of argument counts and types. For example, you might
write a function to produce a Date that takes either a timestamp (one argument) or a month/day/year
specification (three arguments).
In TypeScript, we can specify a function that can be called in different ways by writing overload signatures.
To do this, write some number of function signatures (usually two or more), followed by the body of the
function:
// @errors: 2575
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
203
typescript
In this example, we wrote two overloads: one accepting one argument, and another accepting three argu-
ments. These first two signatures are called the overload signatures.
Then, we wrote a function implementation with a compatible signature. Functions have an implementation
signature, but this signature can't be called directly. Even though we wrote a function with two optional pa-
rameters after the required one, it can't be called with two parameters!
// @errors: 2554
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
Again, the signature used to write the function body can't be "seen" from the outside.
The signature of the implementation is not visible from the outside. When writing an overloaded func-
tion, you should always have two or more signatures above the implementation of the function.
The implementation signature must also be compatible with the overload signatures. For example, these
functions have errors because the implementation signature doesn't match the overloads in a correct way:
// @errors: 2394
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
function fn(x: boolean) {}
// @errors: 2394
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
function fn(x: string | number) {
return "oops";
}
204
More on Functions
This function is fine; we can invoke it with strings or arrays. However, we can't invoke it with a value that
might be a string or an array, because TypeScript can only resolve a function call to a single overload:
// @errors: 2769
declare function len(s: string): number;
declare function len(arr: any[]): number;
// ---cut---
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
Because both overloads have the same argument count and same return type, we can instead write a non-
overloaded version of the function:
This is much better! Callers can invoke this with either sort of value, and as an added bonus, we don't have
to figure out a correct implementation signature.
Always prefer parameters with union types instead of overloads when possible
const user = {
id: 123,
admin: false,
becomeAdmin: function () {
this.admin = true;
},
};
TypeScript understands that the function user.becomeAdmin has a corresponding this which is the outer
object user . this , heh, can be enough for a lot of cases, but there are a lot of cases where you need
more control over what object this represents. The JavaScript specification states that you cannot have a
parameter called this , and so TypeScript uses that syntax space to let you declare the type for this in
the function body.
205
typescript
interface User {
id: number;
admin: boolean;
}
declare const getDB: () => DB;
// ---cut---
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
const admins = db.filterUsers(function (this: User) {
return this.admin;
});
This pattern is common with callback-style APIs, where another object typically controls when your function
is called. Note that you need to use function and not arrow functions to get this behavior:
const db = getDB();
const admins = db.filterUsers(() => this.admin);
void
void represents the return value of functions which don't return a value. It's the inferred type any time a
function doesn't have any return statements, or doesn't return any explicit value from those return
statements:
In JavaScript, a function that doesn't return any value will implicitly return the value undefined . However,
void and undefined are not the same thing in TypeScript. There are further details at the end of this
chapter.
206
More on Functions
object
The special type object refers to any value that isn't a primitive ( string , number , bigint , boolean ,
symbol , null , or undefined ). This is different from the empty object type { } , and also different from
the global type Object . It's very likely you will never use Object .
Note that in JavaScript, function values are objects: They have properties, have Object.prototype in their
prototype chain, are instanceof Object , you can call Object.keys on them, and so on. For this reason,
function types are considered to be object s in TypeScript.
unknown
The unknown type represents any value. This is similar to the any type, but is safer because it's not legal
to do anything with an unknown value:
// @errors: 2571
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
}
This is useful when describing function types because you can describe functions that accept any value with-
out having any values in your function body.
Conversely, you can describe a function that returns a value of unknown type:
never
Some functions never return a value:
207
typescript
The never type represents values which are never observed. In a return type, this means that the function
throws an exception or terminates execution of the program.
never also appears when TypeScript determines there's nothing left in a union.
Function
The global type Function describes properties like bind , call , apply , and others present on all func-
tion values in JavaScript. It also has the special property that values of type Function can always be
called; these calls return any :
This is an untyped function call and is generally best avoided because of the unsafe any return type.
If you need to accept an arbitrary function but don't intend to call it, the type () => void is generally
safer.
Background Reading:
Rest Parameters
Spread Syntax
Rest Parameters
In addition to using optional parameters or overloads to make functions that can accept a variety of fixed
argument counts, we can also define functions that take an unbounded number of arguments using rest
parameters.
A rest parameter appears after all other parameters, and uses the ... syntax:
208
More on Functions
In TypeScript, the type annotation on these parameters is implicitly any[] instead of any , and any type
annotation given must be of the form Array<T> or T[] , or a tuple type (which we'll learn about later).
Rest Arguments
Conversely, we can provide a variable number of arguments from an array using the spread syntax. For ex-
ample, the push method of arrays takes any number of arguments:
Note that in general, TypeScript does not assume that arrays are immutable. This can lead to some surpris-
ing behavior:
// @errors: 2556
// Inferred type is number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);
The best fix for this situation depends a bit on your code, but in general a const context is the most
straightforward solution:
Using rest arguments may require turning on downlevelIteration when targeting older runtimes.
Parameter Destructuring
Background Reading:
Destructuring Assignment
You can use parameter destructuring to conveniently unpack objects provided as an argument into one or
more local variables in the function body. In JavaScript, it looks like this:
function sum({ a, b, c }) {
console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });
The type annotation for the object goes after the destructuring syntax:
209
typescript
This can look a bit verbose, but you can use a named type here as well:
Assignability of Functions
Return type void
The void return type for functions can produce some unusual, but expected behavior.
Contextual typing with a return type of void does not force functions to not return something. Another
way to say this is a contextual function type with a void return type ( type vf = () => void ), when im-
plemented, can return any other value, but it will be ignored.
Thus, the following implementations of the type () => void are valid:
And when the return value of one of these functions is assigned to another variable, it will retain the type of
void :
const v2 = f2();
const v3 = f3();
This behavior exists so that the following code is valid even though Array.prototype.push returns a num-
ber and the Array.prototype.forEach method expects a function with a return type of void .
210
More on Functions
There is one other special case to be aware of, when a literal function definition has a void return type,
that function must not return anything.
v1 handbook
v2 handbook
FAQ - "Why are functions returning non-void assignable to function returning void?"
Go to TOC
211
typescript
If padding is a number , it will treat that as the number of spaces we want to prepend to input . If pad‐
ding is a string , it should just prepend padding to input . Let's try to implement the logic for when
padLeft is passed a number for padding .
// @errors: 2345
function padLeft(padding: number | string, input: string) {
return " ".repeat(padding) + input;
}
Uh-oh, we're getting an error on padding . TypeScript is warning us that adding a number | string to a
number might not give us what we want, and it's right. In other words, we haven't explicitly checked if
padding is a number first, nor are we handling the case where it's a string , so let's do exactly that.
If this mostly looks like uninteresting JavaScript code, that's sort of the point. Apart from the annotations
we put in place, this TypeScript code looks like JavaScript. The idea is that TypeScript's type system aims to
make it as easy as possible to write typical JavaScript code without bending over backwards to get type
safety.
While it might not look like much, there's actually a lot going under the covers here. Much like how
TypeScript analyzes runtime values using static types, it overlays type analysis on JavaScript's runtime con-
trol flow constructs like if/else , conditional ternaries, loops, truthiness checks, etc., which can all affect
those types.
Within our if check, TypeScript sees typeof padding === "number" and understands that as a special
form of code called a type guard. TypeScript follows possible paths of execution that our programs can take
to analyze the most specific possible type of a value at a given position. It looks at these special checks
(called type guards) and assignments, and the process of refining types to more specific types than de-
clared is called narrowing. In many editors we can observe these types as they change, and we'll even do
so in our examples.
212
Narrowing
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
Like we saw with padLeft , this operator comes up pretty often in a number of JavaScript libraries, and
TypeScript can understand it to narrow types in different branches.
In TypeScript, checking against the value returned by typeof is a type guard. Because TypeScript encodes
how typeof operates on different values, it knows about some of its quirks in JavaScript. For example, no-
tice that in the list above, typeof doesn't return the string null . Check out the following example:
// @errors: 2531
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
In the printAll function, we try to check if strs is an object to see if it's an array type (now might be a
good time to reinforce that arrays are object types in JavaScript). But it turns out that in JavaScript, type‐
of null is actually "object" ! This is one of those unfortunate accidents of history.
Users with enough experience might not be surprised, but not everyone has run into this in JavaScript;
luckily, TypeScript lets us know that strs was only narrowed down to string[] | null instead of just
string[] .
This might be a good segue into what we'll call "truthiness" checking.
213
typescript
Truthiness narrowing
Truthiness might not be a word you'll find in the dictionary, but it's very much something you'll hear about
in JavaScript.
In JavaScript, we can use any expression in conditionals, && s, || s, if statements, Boolean negations
( ! ), and more. As an example, if statements don't expect their condition to always have the type bool‐
ean .
In JavaScript, constructs like if first "coerce" their conditions to boolean s to make sense of them, and
then choose their branches depending on whether the result is true or false . Values like
0
NaN
"" (the empty string)
0n (the bigint version of zero)
null
undefined
all coerce to false , and other values get coerced true . You can always coerce values to boolean s by
running them through the Boolean function, or by using the shorter double-Boolean negation. (The latter
has the advantage that TypeScript infers a narrow literal boolean type true , while inferring the first as
type boolean .)
It's fairly popular to leverage this behavior, especially for guarding against values like null or undefined .
As an example, let's try using it for our printAll function.
You'll notice that we've gotten rid of the error above by checking if strs is truthy. This at least prevents us
from dreaded errors when we run our code like:
214
Narrowing
Keep in mind though that truthiness checking on primitives can often be error prone. As an example, con-
sider a different attempt at writing printAll
We wrapped the entire body of the function in a truthy check, but this has a subtle downside: we may no
longer be handling the empty string case correctly.
TypeScript doesn't hurt us here at all, but this is behavior worth noting if you're less familiar with
JavaScript. TypeScript can often help you catch bugs early on, but if you choose to do nothing with a value,
there's only so much that it can do without being overly prescriptive. If you want, you can make sure you
handle situations like these with a linter.
One last word on narrowing by truthiness is that Boolean negations with ! filter out from negated
branches.
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
Equality narrowing
TypeScript also uses switch statements and equality checks like === , !== , == , and != to narrow
types. For example:
215
typescript
} else {
console.log(x);
// ^?
console.log(y);
// ^?
}
}
When we checked that x and y are both equal in the above example, TypeScript knew their types also
had to be equal. Since string is the only common type that both x and y could take on, TypeScript
knows that x and y must be a string in the first branch.
Checking against specific literal values (as opposed to variables) works also. In our section about truthiness
narrowing, we wrote a printAll function which was error-prone because it accidentally didn't handle emp-
ty strings properly. Instead we could have done a specific check to block out null s, and TypeScript still
correctly removes null from the type of strs .
JavaScript's looser equality checks with == and != also get narrowed correctly. If you're unfamiliar, check-
ing whether something == null actually not only checks whether it is specifically the value null - it also
checks whether it's potentially undefined . The same applies to == undefined : it checks whether a value
is either null or undefined .
interface Container {
value: number | null | undefined;
}
216
Narrowing
For example, with the code: "value" in x . where "value" is a string literal and x is a union type. The
"true" branch narrows x 's types which have either an optional or required property value , and the "false"
branch narrows to types which have an optional or missing property value .
return animal.fly();
}
To reiterate optional properties will exist in both sides for narrowing, for example a human could both swim
and fly (with the right equipment) and thus should show up in both sides of the in check:
instanceof narrowing
JavaScript has an operator for checking whether or not a value is an "instance" of another value. More
specifically, in JavaScript x instanceof Foo checks whether the prototype chain of x contains
Foo.prototype . While we won't dive deep here, and you'll see more of this when we get into classes, they
can still be useful for most values that can be constructed with new . As you might have guessed, in‐
stanceof is also a type guard, and TypeScript narrows in branches guarded by instanceof s.
217
typescript
Assignments
As we mentioned earlier, when we assign to any variable, TypeScript looks at the right side of the assign-
ment and narrows the left side appropriately.
console.log(x);
// ^?
x = "goodbye!";
console.log(x);
// ^?
Notice that each of these assignments is valid. Even though the observed type of x changed to number af-
ter our first assignment, we were still able to assign a string to x . This is because the declared type of
x - the type that x started with - is string | number , and assignability is always checked against the
declared type.
If we'd assigned a boolean to x , we'd have seen an error since that wasn't part of the declared type.
// @errors: 2322
let x = Math.random() < 0.5 ? 10 : "hello world!";
// ^?
x = 1;
console.log(x);
// ^?
x = true;
console.log(x);
// ^?
padLeft returns from within its first if block. TypeScript was able to analyze this code and see that the
rest of the body ( return padding + input; ) is unreachable in the case where padding is a number . As
a result, it was able to remove number from the type of padding (narrowing from string | number to
string ) for the rest of the function.
218
Narrowing
This analysis of code based on reachability is called control flow analysis, and TypeScript uses this flow
analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, con-
trol flow can split off and re-merge over and over again, and that variable can be observed to have a differ-
ent type at each point.
function example() {
let x: string | number | boolean;
console.log(x);
// ^?
return x;
// ^?
}
To define a user-defined type guard, we simply need to define a function whose return type is a type
predicate:
pet is Fish is our type predicate in this example. A predicate takes the form parameterName is Type ,
where parameterName must be the name of a parameter from the current function signature.
Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if
the original type is compatible.
219
typescript
// ---cut---
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
Notice that TypeScript not only knows that pet is a Fish in the if branch; it also knows that in the
else branch, you don't have a Fish , so you must have a Bird .
You may use the type guard isFish to filter an array of Fish | Bird and obtain an array of Fish :
Discriminated unions
Most of the examples we've looked at so far have focused around narrowing single variables with simple
types like string , boolean , and number . While this is common, most of the time in JavaScript we'll be
dealing with slightly more complex structures.
For some motivation, let's imagine we're trying to encode shapes like circles and squares. Circles keep track
of their radiuses and squares keep track of their side lengths. We'll use a field called kind to tell which
shape we're dealing with. Here's a first attempt at defining Shape .
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
Notice we're using a union of string literal types: "circle" and "square" to tell us whether we should
treat the shape as a circle or square respectively. By using "circle" | "square" instead of string , we
can avoid misspelling issues.
220
Narrowing
// @errors: 2367
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
// ---cut---
function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
// ...
}
}
We can write a getArea function that applies the right logic based on if it's dealing with a circle or square.
We'll first try dealing with circles.
// @errors: 2532
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
// ---cut---
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
}
Under strictNullChecks that gives us an error - which is appropriate since radius might not be
defined. But what if we perform the appropriate checks on the kind property?
// @errors: 2532
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
// ---cut---
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}
Hmm, TypeScript still doesn't know what to do here. We've hit a point where we know more about our val-
ues than the type checker does. We could try to use a non-null assertion (a ! after shape.radius ) to say
that radius is definitely present.
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
// ---cut---
221
typescript
But this doesn't feel ideal. We had to shout a bit at the type-checker with those non-null assertions ( ! ) to
convince it that shape.radius was defined, but those assertions are error-prone if we start to move code
around. Additionally, outside of strictNullChecks we're able to accidentally access any of those fields
anyway (since optional properties are just assumed to always be present when reading them). We can defi-
nitely do better.
The problem with this encoding of Shape is that the type-checker doesn't have any way to know whether
or not radius or sideLength are present based on the kind property. We need to communicate what
we know to the type checker. With that in mind, let's take another swing at defining Shape .
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
Here, we've properly separated Shape out into two types with different values for the kind property, but
radius and sideLength are declared as required properties in their respective types.
Let's see what happens here when we try to access the radius of a Shape .
// @errors: 2339
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// ---cut---
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
}
222
Narrowing
Like with our first definition of Shape , this is still an error. When radius was optional, we got an error
(with strictNullChecks enabled) because TypeScript couldn't tell whether the property was present. Now
that Shape is a union, TypeScript is telling us that shape might be a Square , and Square s don't have
radius defined on them! Both interpretations are correct, but only the union encoding of Shape will cause
an error regardless of how strictNullChecks is configured.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// ---cut---
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
// ^?
}
}
That got rid of the error! When every type in a union contains a common property with literal types,
TypeScript considers that to be a discriminated union, and can narrow out the members of the union.
In this case, kind was that common property (which is what's considered a discriminant property of
Shape ). Checking whether the kind property was "circle" got rid of every type in Shape that didn't
have a kind property with the type "circle" . That narrowed shape down to the type Circle .
The same checking works with switch statements as well. Now we can try to write our complete getArea
without any pesky ! non-null assertions.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// ---cut---
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
// ^?
223
typescript
case "square":
return shape.sideLength ** 2;
// ^?
}
}
The important thing here was the encoding of Shape . Communicating the right information to TypeScript -
that Circle and Square were really two separate types with specific kind fields - was crucial. Doing that
let us write type-safe TypeScript code that looks no different than the JavaScript we would've written other-
wise. From there, the type system was able to do the "right" thing and figure out the types in each branch
of our switch statement.
As an aside, try playing around with the above example and remove some of the return keywords.
You'll see that type-checking can help avoid bugs when accidentally falling through different clauses in
a switch statement.
Discriminated unions are useful for more than just talking about circles and squares. They're good for repre-
senting any sort of messaging scheme in JavaScript, like when sending messages over the network
(client/server communication), or encoding mutations in a state management framework.
Exhaustiveness checking
The never type is assignable to every type; however, no type is assignable to never (except never
itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a
switch statement.
For example, adding a default to our getArea function which tries to assign the shape to never will
raise when every possible case has not been handled.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// ---cut---
type Shape = Circle | Square;
224
Narrowing
Adding a new member to the Shape union, will cause a TypeScript error:
// @errors: 2322
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// ---cut---
interface Triangle {
kind: "triangle";
sideLength: number;
}
Go to TOC
225
typescript
In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript,
we represent those through object types.
interface Person {
// ^^^^^^
name: string;
age: number;
}
or a type alias.
type Person = {
// ^^^^^^
name: string;
age: number;
};
In all three examples above, we've written functions that take objects that contain the property name
(which must be a string ) and age (which must be a number ).
Property Modifiers
Each property in an object type can specify a couple of things: the type, whether the property is optional,
and whether the property can be written to.
Optional Properties
Much of the time, we'll find ourselves dealing with objects that might have a property set. In those cases,
we can mark those properties as optional by adding a question mark ( ? ) to the end of their names.
interface Shape {}
declare function getShape(): Shape;
// ---cut---
interface PaintOptions {
shape: Shape;
xPos?: number;
226
Object Types
// ^
yPos?: number;
// ^
}
In this example, both xPos and yPos are considered optional. We can choose to provide either of them,
so every call above to paintShape is valid. All optionality really says is that if the property is set, it better
have a specific type.
We can also read from those properties - but when we do under strictNullChecks , TypeScript will tell us
they're potentially undefined .
interface Shape {}
declare function getShape(): Shape;
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
// ---cut---
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
// ^?
let yPos = opts.yPos;
// ^?
// ...
}
In JavaScript, even if the property has never been set, we can still access it - it's just going to give us the
value undefined . We can just handle undefined specially.
interface Shape {}
declare function getShape(): Shape;
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
// ---cut---
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos === undefined ? 0 : opts.xPos;
// ^?
let yPos = opts.yPos === undefined ? 0 : opts.yPos;
227
typescript
// ^?
// ...
}
Note that this pattern of setting defaults for unspecified values is so common that JavaScript has syntax to
support it.
interface Shape {}
declare function getShape(): Shape;
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
// ---cut---
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
// ^?
console.log("y coordinate at", yPos);
// ^?
// ...
}
Here we used a destructuring pattern for paintShape 's parameter, and provided default values for xPos
and yPos . Now xPos and yPos are both definitely present within the body of paintShape , but optional
for any callers to paintShape .
Note that there is currently no way to place type annotations within destructuring patterns. This is be-
cause the following syntax already means something different in JavaScript.
// @noImplicitAny: false
// @errors: 2552 2304
interface Shape {}
declare function render(x: unknown);
// ---cut---
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
render(xPos);
}
In an object destructuring pattern, shape: Shape means "grab the property shape and redefine it locally
as a variable named Shape . Likewise xPos: number creates a variable named number whose value is
based on the parameter's xPos .
readonly Properties
Properties can also be marked as readonly for TypeScript. While it won't change any behavior at runtime,
a property marked as readonly can't be written to during type-checking.
228
Object Types
// @errors: 2540
interface SomeType {
readonly prop: string;
}
Using the readonly modifier doesn't necessarily imply that a value is totally immutable - or in other
words, that its internal contents can't be changed. It just means the property itself can't be re-written to.
// @errors: 2540
interface Home {
readonly resident: { name: string; age: number };
}
It's important to manage expectations of what readonly implies. It's useful to signal intent during devel-
opment time for TypeScript on how an object should be used. TypeScript doesn't factor in whether proper-
ties on two types are readonly when checking whether those types are compatible, so readonly proper-
ties can also change via aliasing.
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
229
typescript
Index Signatures
Sometimes you don't know all the names of a type's properties ahead of time, but you do know the shape
of the values.
In those cases you can use an index signature to describe the types of possible values, for example:
Above, we have a StringArray interface which has an index signature. This index signature states that
when a StringArray is indexed with a number , it will return a string .
Only some types are allowed for index signature properties: string , number , symbol , template string
patterns, and union types consisting only of these.
230
Object Types
Details
While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that
all properties match their return type. This is because a string index declares that obj.property is also
available as obj["property"] . In the following example, name 's type does not match the string index's
type, and the type checker gives an error:
// @errors: 2411
// @errors: 2411
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
}
However, properties of different types are acceptable if the index signature is a union of the property types:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Finally, you can make index signatures readonly in order to prevent assignment to their indices:
Extending Types
It's pretty common to have types that might be more specific versions of other types. For example, we
might have a BasicAddress type that describes the fields necessary for sending letters and packages in
the U.S.
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
In some situations that's enough, but addresses often have a unit number associated with them if the build-
ing at an address has multiple units. We can then describe an AddressWithUnit .
231
typescript
interface AddressWithUnit {
name?: string;
unit: string;
//^^^^^^^^^^^^^
street: string;
city: string;
country: string;
postalCode: string;
}
This does the job, but the downside here is that we had to repeat all the other fields from BasicAddress
when our changes were purely additive. Instead, we can extend the original BasicAddress type and just
add the new fields that are unique to AddressWithUnit .
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
The extends keyword on an interface allows us to effectively copy members from other named types,
and add whatever new members we want. This can be useful for cutting down the amount of type declara-
tion boilerplate we have to write, and for signaling intent that several different declarations of the same
property might be related. For example, AddressWithUnit didn't need to repeat the street property, and
because street originates from BasicAddress , a reader will know that those two types are related in
some way.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
Intersection Types
interface s allowed us to build up new types from other types by extending them. TypeScript provides an-
other construct called intersection types that is mainly used to combine existing object types.
232
Object Types
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
Here, we've intersected Colorful and Circle to produce a new type that has all the members of
Colorful and Circle .
// @errors: 2345
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
// ---cut---
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 });
interface Box {
contents: any;
}
Right now, the contents property is typed as any , which works, but can lead to accidents down the line.
We could instead use unknown , but that would mean that in cases where we already know the type of
contents , we'd need to do precautionary checks, or use error-prone type assertions.
233
typescript
interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
One type safe approach would be to instead scaffold out different Box types for every type of contents .
// @errors: 2322
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}
But that means we'll have to create different functions, or overloads of functions, to operate on these types.
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}
// ---cut---
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}
That's a lot of boilerplate. Moreover, we might later need to introduce new types and overloads. This is frus-
trating, since our box types and overloads are all effectively the same.
Instead, we can make a generic Box type which declares a type parameter.
234
Object Types
interface Box<Type> {
contents: Type;
}
You might read this as “A Box of Type is something whose contents have type Type ”. Later on, when
we refer to Box , we have to give a type argument in place of Type .
interface Box<Type> {
contents: Type;
}
// ---cut---
let box: Box<string>;
Think of Box as a template for a real type, where Type is a placeholder that will get replaced with some
other type. When TypeScript sees Box<string> , it will replace every instance of Type in Box<Type> with
string , and end up working with something like { contents: string } . In other words, Box<string>
and our earlier StringBox work identically.
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
Box is reusable in that Type can be substituted with anything. That means that when we need a box for a
new type, we don't need to declare a new Box type at all (though we certainly could if we wanted to).
interface Box<Type> {
contents: Type;
}
interface Apple {
// ....
}
This also means that we can avoid overloads entirely by instead using generic functions.
interface Box<Type> {
contents: Type;
}
// ---cut---
235
typescript
It is worth noting that type aliases can also be generic. We could have defined our new Box<Type> inter-
face, which was:
interface Box<Type> {
contents: Type;
}
type Box<Type> = {
contents: Type;
};
Since type aliases, unlike interfaces, can describe more than just object types, we can also use them to
write other kinds of generic helper types.
// @errors: 2575
type OrNull<Type> = Type | null;
It turns out we've been working with a type just like that throughout this handbook: the Array type.
Whenever we write out types like number[] or string[] , that's really just a shorthand for
Array<number> and Array<string> .
Much like the Box type above, Array itself is a generic type.
236
Object Types
// @noLib: true
interface Number {}
interface String {}
interface Boolean {}
interface Symbol {}
// ---cut---
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
// ...
}
Modern JavaScript also provides other data structures which are generic, like Map<K, V> , Set<T> , and
Promise<T> . All this really means is that because of how Map , Set , and Promise behave, they can work
with any sets of types.
// @errors: 2339
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
Much like the readonly modifier for properties, it's mainly a tool we can use for intent. When we see a
function that returns ReadonlyArray s, it tells us we're not meant to change the contents at all, and when
we see a function that consumes ReadonlyArray s, it tells us that we can pass any array into that function
without worrying that it will change its contents.
// @errors: 2693
new ReadonlyArray("red", "green", "blue");
237
typescript
Just as TypeScript provides a shorthand syntax for Array<Type> with Type[] , it also provides a shorthand
syntax for ReadonlyArray<Type> with readonly Type[] .
// @errors: 2339
function doStuff(values: readonly string[]) {
// ^^^^^^^^^^^^^^^^^
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
One last thing to note is that unlike the readonly property modifier, assignability isn't bidirectional be-
tween regular Array s and ReadonlyArray s.
// @errors: 4104
let x: readonly string[] = [];
let y: string[] = [];
x = y;
y = x;
Tuple Types
A tuple type is another sort of Array type that knows exactly how many elements it contains, and exactly
which types it contains at specific positions.
Here, StringNumberPair is a tuple type of string and number . Like ReadonlyArray , it has no repre-
sentation at runtime, but is significant to TypeScript. To the type system, StringNumberPair describes ar-
rays whose 0 index contains a string and whose 1 index contains a number .
doSomething(["hello", 42]);
// @errors: 2493
function doSomething(pair: [string, number]) {
// ...
const c = pair[2];
}
238
Object Types
console.log(inputString);
// ^?
console.log(hash);
// ^?
}
Tuple types are useful in heavily convention-based APIs, where each element's meaning is "obvious".
This gives us flexibility in whatever we want to name our variables when we destructure them. In the
above example, we were able to name elements 0 and 1 to whatever we wanted.
However, since not every user holds the same view of what's obvious, it may be worth reconsidering
whether using objects with descriptive property names may be better for your API.
Other than those length checks, simple tuple types like these are equivalent to types which are versions of
Array s that declare properties for specific indexes, and that declare length with a numeric literal type.
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
Another thing you may be interested in is that tuples can have optional properties by writing out a question
mark ( ? after an element's type). Optional tuple elements can only come at the end, and also affect the
type of length .
Tuples can also have rest elements, which have to be an array/tuple type.
239
typescript
StringNumberBooleans describes a tuple whose first two elements are string and number respec-
tively, but which may have any number of boolean s following.
StringBooleansNumber describes a tuple whose first element is string and then any number of
boolean s and ending with a number .
BooleansStringNumber describes a tuple whose starting elements are any number of boolean s and
ending with a string then a number .
A tuple with a rest element has no set "length" - it only has a set of well-known elements in different
positions.
Why might optional and rest elements be useful? Well, it allows TypeScript to correspond tuples with para-
meter lists. Tuples types can be used in rest parameters and arguments, so that the following:
This is handy when you want to take a variable number of arguments with a rest parameter, and you need a
minimum number of elements, but you don't want to introduce intermediate variables.
As you might expect, writing to any property of a readonly tuple isn't allowed in TypeScript.
// @errors: 2540
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
}
240
Object Types
Tuples tend to be created and left un-modified in most code, so annotating types as readonly tuples when
possible is a good default. This is also important given that array literals with const assertions will be in-
ferred with readonly tuple types.
// @errors: 2345
let point = [3, 4] as const;
distanceFromOrigin(point);
Here, distanceFromOrigin never modifies its elements, but expects a mutable tuple. Since point 's type
was inferred as readonly [3, 4] , it won't be compatible with [number, number] since that type can't
guarantee point 's elements won't be mutated.
Go to TOC
241
typescript
The most common kinds of errors that programmers write can be described as type errors: a certain kind of
value was used where a different kind of value was expected. This could be due to simple typos, a failure to
understand the API surface of a library, incorrect assumptions about runtime behavior, or other errors. The
goal of TypeScript is to be a static typechecker for JavaScript programs - in other words, a tool that runs
before your code runs (static) and ensures that the types of the program are correct (typechecked).
If you are coming to TypeScript without a JavaScript background, with the intention of TypeScript being
your first language, we recommend you first start reading the documentation on either the Microsoft Learn
JavaScript tutorial or read JavaScript at the Mozilla Web Docs. If you have experience in other languages,
you should be able to pick up JavaScript syntax quite quickly by reading the handbook.
The Handbook
The TypeScript Handbook is intended to be a comprehensive document that explains TypeScript to every-
day programmers. You can read the handbook by going from top to bottom in the left-hand navigation.
You should expect each chapter or page to provide you with a strong understanding of the given con-
cepts. The TypeScript Handbook is not a complete language specification, but it is intended to be a com-
prehensive guide to all of the language's features and behaviors.
In the interests of clarity and brevity, the main content of the Handbook will not explore every edge case
or minutiae of the features being covered. You can find more details on particular concepts in the refer-
ence articles.
Reference Files
242
The TypeScript Handbook
The reference section below the handbook in the navigation is built to provide a richer understanding of
how a particular part of TypeScript works. You can read it top-to-bottom, but each section aims to pro-
vide a deeper explanation of a single concept - meaning there is no aim for continuity.
Non-Goals
The Handbook is also intended to be a concise document that can be comfortably read in a few hours.
Certain topics won't be covered in order to keep things short.
Specifically, the Handbook does not fully introduce core JavaScript basics like functions, classes, and clo-
sures. Where appropriate, we'll include links to background reading that you can use to read up on those
concepts.
The Handbook also isn't intended to be a replacement for a language specification. In some cases, edge
cases or formal descriptions of behavior will be skipped in favor of high-level, easier-to-understand explana-
tions. Instead, there are separate reference pages that more precisely and formally describe many aspects
of TypeScript's behavior. The reference pages are not intended for readers unfamiliar with TypeScript, so
they may use advanced terminology or reference topics you haven't read about yet.
Finally, the Handbook won't cover how TypeScript interacts with other tools, except where necessary. Topics
like how to configure TypeScript with webpack, rollup, parcel, react, babel, closure, lerna, rush, bazel, pre-
act, vue, angular, svelte, jquery, yarn, or npm are out of scope - you can find these resources elsewhere on
the web.
Get Started
Before getting started with The Basics, we recommend reading one of the following introductory pages.
These introductions are intended to highlight key similarities and differences between TypeScript and your
favored programming language, and clear up common misconceptions specific to those languages.
Go to TOC
243
typescript
Throughout the sections you've read so far, we've been demonstrating basic TypeScript concepts using the
built-in functions present in all JavaScript runtimes. However, almost all JavaScript today includes many li-
braries to accomplish common tasks. Having types for the parts of your application that aren't your code
will greatly improve your TypeScript experience. Where do these types come from?
// @errors: 2339
const k = Math.max(5, 6);
const j = Math.mix(7, 8);
How did TypeScript know that max was present but not mix , even though Math 's implementation wasn't
part of your code?
The answer is that there are declaration files describing these built-in objects. A declaration file provides a
way to declare the existence of some types or values without actually providing implementations for those
values.
.d.ts files
TypeScript has two main kinds of files. .ts files are implementation files that contain types and executable
code. These are the files that produce .js outputs, and are where you'd normally write your code.
.d.ts files are declaration files that contain only type information. These files don't produce .js outputs;
they are only used for typechecking. We'll learn more about how to write our own declaration files later.
TypeScript names these declaration files with the pattern lib.[something].d.ts . If you navigate into a
file with that name, you can know that you're dealing with some built-in part of the platform, not user code.
target setting
The methods, properties, and functions available to you actually vary based on the version of JavaScript
your code is running on. For example, the startsWith method of strings is available only starting with the
version of JavaScript referred as ECMAScript 6.
244
Type Declarations
Being aware of what version of JavaScript your code ultimately runs on is important because you don't want
to use APIs that are from a newer version than the platform you deploy to. This is one function of the tar‐
get compiler setting.
TypeScript helps with this problem by varying which lib files are included by default based on your tar‐
get setting. For example, if target is ES5 , you will see an error if trying to use the startsWith method,
because that method is only available in ES6 or later.
lib setting
The lib setting allows more fine-grained control of which built-in declaration files are considered available
in your program. See the documentation page on lib for more information.
External Definitions
For non-built-in APIs, there are a variety of ways you can get declaration files. How you do this depends on
exactly which library you're getting types for.
Bundled Types
If a library you're using is published as an npm package, it may include type declaration files as part of its
distribution already. You can read the project's documentation to find out, or simply try importing the pack-
age and see if TypeScript is able to automatically resolve the types for you.
If you're a package author considering bundling type definitions with your package, you can read our guide
on bundling type definitions.
DefinitelyTyped / @types
The DefinitelyTyped repository is a centralized repo storing declaration files for thousands of libraries. The
vast majority of commonly-used libraries have declaration files available on DefinitelyTyped.
Definitions on DefinitelyTyped are also automatically published to npm under the @types scope. The name
of the types package is always the same as the name of the underlying package itself. For example, if you
installed the react npm package, you can install its corresponding types by running
TypeScript automatically finds type definitions under node_modules/@types , so there's no other step need-
ed to get these types available in your program.
245
typescript
If you want to silence warnings about a particular module without writing a declaration file, you can also
quick declare the module as type any by putting an empty declaration for it in a .d.ts file in your project.
For example, if you wanted to use a module named some-untyped-module without having definitions for it,
you would write:
Go to TOC
246
Conditional Types
At the heart of most useful programs, we have to make decisions based on input. JavaScript programs are
no different, but given the fact that values can be easily introspected, those decisions are also based on the
types of the inputs. Conditional types help describe the relation between the types of inputs and outputs.
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
Conditional types take a form that looks a little like conditional expressions ( condition ? trueExpression
: falseExpression ) in JavaScript:
When the type on the left of the extends is assignable to the one on the right, then you'll get the type in
the first branch (the "true" branch); otherwise you'll get the type in the latter branch (the "false" branch).
From the examples above, conditional types might not immediately seem useful - we can tell ourselves
whether or not Dog extends Animal and pick number or string ! But the power of conditional types
comes from using them with generics.
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
These overloads for createLabel describe a single JavaScript function that makes a choice based on the
types of its inputs. Note a few things:
247
typescript
1. If a library has to make the same sort of choice over and over throughout its API, this becomes
cumbersome.
2. We have to create three overloads: one for each case when we're sure of the type (one for string and
one for number ), and one for the most general case (taking a string | number ). For every new type
createLabel can handle, the number of overloads grows exponentially.
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
// ---cut---
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
We can then use that conditional type to simplify our overloads down to a single function with no overloads.
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
// ---cut---
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
// ^?
let b = createLabel(2.8);
// ^?
// @errors: 2536
type MessageOf<T> = T["message"];
248
Conditional Types
In this example, TypeScript errors because T isn't known to have a property called message . We could
constrain T , and TypeScript would no longer complain:
interface Email {
message: string;
}
However, what if we wanted MessageOf to take any type, and default to something like never if a mes‐
sage property isn't available? We can do this by moving the constraint out and introducing a conditional
type:
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
Within the true branch, TypeScript knows that T will have a message property.
As another example, we could also write a type called Flatten that flattens array types to their element
types, but leaves them alone otherwise:
When Flatten is given an array type, it uses an indexed access with number to fetch out string[] 's
element type. Otherwise, it just returns the type it was given.
249
typescript
Conditional types provide us with a way to infer from types we compare against in the true branch using the
infer keyword. For example, we could have inferred the element type in Flatten instead of fetching it
out "manually" with an indexed access type:
Here, we used the infer keyword to declaratively introduce a new generic type variable named Item in-
stead of specifying how to retrieve the element type of T within the true branch. This frees us from having
to think about how to dig through and probing apart the structure of the types we're interested in.
We can write some useful helper type aliases using the infer keyword. For example, for simple cases, we
can extract the return type out from function types:
When inferring from a type with multiple call signatures (such as the type of an overloaded function), infer-
ences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not
possible to perform overload resolution based on a list of argument types.
If we plug a union type into ToArray , then the conditional type will be applied to each member of that
union.
250
Conditional Types
type StrArrOrNumArr =
// ---cut---
string | number;
and maps over each member type of the union, to what is effectively:
type StrArrOrNumArr =
// ---cut---
string[] | number[];
Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the
extends keyword with square brackets.
Go to TOC
251
typescript
A major part of software engineering is building components that not only have well-defined and consistent
APIs, but are also reusable. Components that are capable of working on the data of today as well as the
data of tomorrow will give you the most flexible capabilities for building up large software systems.
In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is
generics, that is, being able to create a component that can work over a variety of types rather than a sin-
gle one. This allows users to consume these components and use their own types.
Without generics, we would either have to give the identity function a specific type:
Or, we could describe the identity function using the any type:
While using any is certainly generic in that it will cause the function to accept any and all types for the type
of arg , we actually are losing the information about what that type was when the function returns. If we
passed in a number, the only information we have is that any type could be returned.
Instead, we need a way of capturing the type of the argument in such a way that we can also use it to de-
note what is being returned. Here, we will use a type variable, a special kind of variable that works on types
rather than values.
We've now added a type variable Type to the identity function. This Type allows us to capture the type
the user provides (e.g. number ), so that we can use that information later. Here, we use Type again as
the return type. On inspection, we can now see the same type is used for the argument and the return
type. This allows us to traffic that type information in one side of the function and out the other.
We say that this version of the identity function is generic, as it works over a range of types. Unlike us-
ing any , it's also just as precise (i.e., it doesn't lose any information) as the first identity function that
used numbers for the argument and return type.
Once we've written the generic identity function, we can call it in one of two ways. The first way is to pass
all of the arguments, including the type argument, to the function:
252
Generics
Here we explicitly set Type to be string as one of the arguments to the function call, denoted using the
<> around the arguments rather than () .
The second way is also perhaps the most common. Here we use type argument inference -- that is, we
want the compiler to set the value of Type for us automatically based on the type of the argument we pass
in:
Notice that we didn't have to explicitly pass the type in the angle brackets ( <> ); the compiler just looked at
the value "myString" , and set Type to its type. While type argument inference can be a helpful tool to
keep code shorter and more readable, you may need to explicitly pass in the type arguments as we did in
the previous example when the compiler fails to infer the type, as may happen in more complex examples.
What if we want to also log the length of the argument arg to the console with each call? We might be
tempted to write this:
// @errors: 2339
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
When we do, the compiler will give us an error that we're using the .length member of arg , but nowhere
have we said that arg has this member. Remember, we said earlier that these type variables stand in for
any and all types, so someone using this function could have passed in a number instead, which does not
have a .length member.
253
typescript
Let's say that we've actually intended this function to work on arrays of Type rather than Type directly.
Since we're working with arrays, the .length member should be available. We can describe this just like
we would create arrays of other types:
You can read the type of loggingIdentity as "the generic function loggingIdentity takes a type para-
meter Type , and an argument arg which is an array of Type s, and returns an array of Type s." If we
passed in an array of numbers, we'd get an array of numbers back out, as Type would bind to number .
This allows us to use our generic type variable Type as part of the types we're working with, rather than
the whole type, giving us greater flexibility.
You may already be familiar with this style of type from other languages. In the next section, we'll cover
how you can create your own generic types like Array<Type> .
Generic Types
In previous sections, we created generic identity functions that worked over a range of types. In this
section, we'll explore the type of the functions themselves and how to create generic interfaces.
The type of generic functions is just like those of non-generic functions, with the type parameters listed
first, similarly to function declarations:
We could also have used a different name for the generic type parameter in the type, so long as the number
of type variables and how the type variables are used line up.
We can also write the generic type as a call signature of an object literal type:
254
Generics
Which leads us to writing our first generic interface. Let's take the object literal from the previous example
and move it to an interface:
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
In a similar example, we may want to move the generic parameter to be a parameter of the whole
interface. This lets us see what type(s) we're generic over (e.g. Dictionary<string> rather than just
Dictionary ). This makes the type parameter visible to all the other members of the interface.
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
Notice that our example has changed to be something slightly different. Instead of describing a generic
function, we now have a non-generic function signature that is a part of a generic type. When we use
GenericIdentityFn , we now will also need to specify the corresponding type argument (here: number ),
effectively locking in what the underlying call signature will use. Understanding when to put the type para-
meter directly on the call signature and when to put it on the interface itself will be helpful in describing
what aspects of a type are generic.
In addition to generic interfaces, we can also create generic classes. Note that it is not possible to create
generic enums and namespaces.
Generic Classes
A generic class has a similar shape to a generic interface. Generic classes have a generic type parameter list
in angle brackets ( <> ) following the name of the class.
// @strict: false
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
255
typescript
This is a pretty literal use of the GenericNumber class, but you may have noticed that nothing is restricting
it to only use the number type. We could have instead used string or even more complex objects.
// @strict: false
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
// ---cut---
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
Just as with interface, putting the type parameter on the class itself lets us make sure all of the properties
of the class are working with the same type.
As we cover in our section on classes, a class has two sides to its type: the static side and the instance side.
Generic classes are only generic over their instance side rather than their static side, so when working with
classes, static members can not use the class's type parameter.
Generic Constraints
If you remember from an earlier example, you may sometimes want to write a generic function that works
on a set of types where you have some knowledge about what capabilities that set of types will have. In our
loggingIdentity example, we wanted to be able to access the .length property of arg , but the com-
piler could not prove that every type had a .length property, so it warns us that we can't make this
assumption.
// @errors: 2339
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
Instead of working with any and all types, we'd like to constrain this function to work with any and all types
that also have the .length property. As long as the type has this member, we'll allow it, but it's required
to have at least this member. To do so, we must list our requirement as a constraint on what Type can be.
To do so, we'll create an interface that describes our constraint. Here, we'll create an interface that has a
single .length property and then we'll use this interface and the extends keyword to denote our
constraint:
256
Generics
interface Lengthwise {
length: number;
}
Because the generic function is now constrained, it will no longer work over any and all types:
// @errors: 2345
interface Lengthwise {
length: number;
}
Instead, we need to pass in values whose type has all the required properties:
interface Lengthwise {
length: number;
}
// @errors: 2345
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
257
typescript
A more advanced example uses the prototype property to infer and constrain relationships between the
constructor function and the instance side of class types.
// @strict: false
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Go to TOC
258
Indexed Access Types
We can use an indexed access type to look up a specific property on another type:
The indexing type is itself a type, so we can use unions, keyof , or other types entirely:
You'll even see an error if you try to index a property that doesn't exist:
// @errors: 2339
type Person = { age: number; name: string; alive: boolean };
// ---cut---
type I1 = Person["alve"];
Another example of indexing with an arbitrary type is using number to get the type of an array's elements.
We can combine this with typeof to conveniently capture the element type of an array literal:
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
You can only use types when indexing, meaning you can't use a const to make a variable reference:
However, you can use a type alias for a similar style of refactor:
259
typescript
Go to TOC
260
Keyof Type Operator
If the type has a string or number index signature, keyof will return those types instead:
Note that in this example, M is string | number -- this is because JavaScript object keys are always co-
erced to a string, so obj[0] is always the same as obj["0"] .
keyof types become especially useful when combined with mapped types, which we'll learn more about
later.
Go to TOC
261
typescript
When you don't want to repeat yourself, sometimes a type needs to be based on another type.
Mapped types build on the syntax for index signatures, which are used to declare the types of properties
which have not been declared ahead of time:
A mapped type is a generic type which uses a union of PropertyKey s (frequently created via a keyof ) to
iterate through keys to create a type:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
In this example, OptionsFlags will take all the properties from the type Type and change their values to
be a boolean.
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
// ---cut---
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
Mapping Modifiers
There are two additional modifiers which can be applied during mapping: readonly and ? which affect
mutability and optionality respectively.
You can remove or add these modifiers by prefixing with - or + . If you don't add a prefix, then + is
assumed.
type LockedAccount = {
readonly id: string;
readonly name: string;
};
262
Mapped Types
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
You can leverage features like template literal types to create new property names from prior ones:
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () =>
Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
You can filter out keys by producing never via a conditional type:
interface Circle {
kind: "circle";
radius: number;
}
You can map over arbitrary unions, not just unions of string | number | symbol , but unions of any type:
263
typescript
Further Exploration
Mapped types work well with other features in this type manipulation section, for example here is a mapped
type using a conditional type which returns either a true or false depending on whether an object has
the property pii set to the literal true :
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
Go to TOC
264
Template Literal Types
Template literal types build on string literal types, and have the ability to expand into many strings via
unions.
They have the same syntax as template literal strings in JavaScript, but are used in type positions. When
used with concrete literal types, a template literal produces a new string literal type by concatenating the
contents.
When a union is used in the interpolated position, the type is the set of every possible string literal that
could be represented by each union member:
For each interpolated position in the template literal, the unions are cross multiplied:
We generally recommend that people use ahead-of-time generation for large string unions, but this is useful
in smaller cases.
Consider the case where a function ( makeWatchedObject ) adds a new function called on() to a passed
object. In JavaScript, its call might look like: makeWatchedObject(baseObject) . We can imagine the base
object as looking like:
// @noErrors
const passedObject = {
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
};
The on function that will be added to the base object expects two arguments, an eventName (a string )
and a callBack (a function ).
265
typescript
Should be passed a value of the type associated with the name attributeInThePassedObject ; thus,
since firstName is typed as string , the callback for the firstNameChanged event expects a string
to be passed to it at call time. Similarly events associated with age should expect to be called with a
number argument
Should have void return type (for simplicity of demonstration)
The naive function signature of on() might thus be: on(eventName: string, callBack: (newValue:
any) => void) . However, in the preceding description, we identified important type constraints that we'd
like to document in our code. Template Literal types let us bring these constraints into our code.
// @noErrors
declare function makeWatchedObject(obj: any): any;
// ---cut---
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
Notice that on listens on the event "firstNameChanged" , not just "firstName" . Our naive specification
of on() could be made more robust if we were to ensure that the set of eligible event names was con-
strained by the union of attribute names in the watched object with "Changed" added at the end. While we
are comfortable with doing such a calculation in JavaScript i.e. Object.keys(passedObject).map(x =>
`${x}Changed`) , template literals inside the type system provide a similar approach to string
manipulation:
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) =>
void): void;
};
With this, we can build something that errors when given the wrong property:
// @errors: 2345
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) =>
void): void;
};
266
Template Literal Types
// Prevent easy human error (using the key instead of the event name)
person.on("firstName", () => {});
// It's typo-resistant
person.on("frstNameChanged", () => {});
The key insight that makes this possible is this: we can use a function with a generic such that:
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ):
void;
};
267
typescript
When a user calls with the string "firstNameChanged" , TypeScript will try to infer the right type for Key .
To do that, it will match Key against the content prior to "Changed" and infer the string "firstName" .
Once TypeScript figures that out, the on method can fetch the type of firstName on the original object,
which is string in this case. Similarly, when called with "ageChanged" , TypeScript finds the type for the
property age which is number .
Inference can be combined in different ways, often to deconstruct strings, and reconstruct them in different
ways.
Uppercase<StringType>
Converts each character in the string to the uppercase version.
Example
Lowercase<StringType>
Converts each character in the string to the lowercase equivalent.
Example
Capitalize<StringType>
Converts the first character in the string to an uppercase equivalent.
268
Template Literal Types
Example
Uncapitalize<StringType>
Converts the first character in the string to a lowercase equivalent.
Example
Go to TOC
269
typescript
// Prints "string"
console.log(typeof "Hello world");
TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or
property:
let s = "hello";
let n: typeof s;
// ^?
This isn't very useful for basic types, but combined with other type operators, you can use typeof to con-
veniently express many patterns. For an example, let's start by looking at the predefined type
ReturnType<T> . It takes a function type and produces its return type:
// @errors: 2749
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>;
Remember that values and types aren't the same thing. To refer to the type that the value f has, we use
typeof :
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
// ^?
Limitations
TypeScript intentionally limits the sorts of expressions you can use typeof on.
Specifically, it's only legal to use typeof on identifiers (i.e. variable names) or their properties. This helps
avoid the confusing trap of writing code you think is executing, but isn't:
// @errors: 1005
declare const msgbox: () => boolean;
// type msgbox = any;
// ---cut---
// Meant to use = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
270
Creating Types from Types
TypeScript's type system is very powerful because it allows expressing types in terms of other types.
The simplest form of this idea is generics, we actually have a wide variety of type operators available to
use. It's also possible to express types in terms of values that we already have.
By combining various type operators, we can express complex operations and values in a succinct, main-
tainable way. In this section we'll cover ways to express a new type in terms of an existing type or value.
Go to TOC
271
typescript
Understanding Errors
Whenever TypeScript finds an error, it tries to explain what went wrong in as much detail as possible.
Because its type system is structural, this often means providing somewhat lengthy descriptions of where it
found a problem.
Terminology
There is some terminology you'll frequently see in error messages that is helpful to understand.
assignable to
TypeScript considers a type assignable to another type if one is an acceptable substitute for the other. In
other words, a Cat is assignable to an Animal because a Cat is an acceptable substitute for an Animal .
As its name implies, this relationship is used to check the validity of an assignment t = s; by examining
the types of t and s . It's also used to check most other places where two types interact. For example,
when calling a function, each argument's type must be assignable to parameter's declared type.
Informally, if you see T is not assignable to S , you can think of that as TypeScript saying " T and S
are not compatible". However, note that this is a directional relationship: S being assignable to T does not
imply that T is assignable to S .
Examples
Let's look at some example error messages and understand what's going on.
Error Elaborations
Each error starts with a leading message, sometimes followed by more sub-messages. You can think of
each sub-message as answering a "why?" question about the message above it. Let's work through some
examples to see how they work in practice.
Here's an example that produces an error message longer than the example itself:
// @errors: 2322
let a: { m: number[] };
let b = { m: [""] };
a = b;
TypeScript found an error when checking the last line. Its logic for issuing an error follows from its logic for
determining if the assignment is OK:
272
Understanding Errors
Extra Properties
// @errors: 2322
type A = { m: number };
const a: A = { m: 10, n: "" };
Union Assignments
// @errors: 2322
type Thing = "none" | { name: string };
Go to TOC
273
typescript
With TypeScript 3.7, TypeScript added support for generating .d.ts files from JavaScript using JSDoc syntax.
This set up means you can own the editor experience of TypeScript-powered editors without porting your
project to TypeScript, or having to maintain .d.ts files in your codebase. TypeScript supports most JSDoc
tags, you can find the reference here.
Adding TypeScript
You can learn how to do this in our installation page.
TSConfig
The TSConfig is a jsonc file which configures both your compiler flags, and declare where to find files. In
this case, you will want a file like the following:
{
// Change this to match your project
"include": ["src/**/*"],
"compilerOptions": {
// Tells TypeScript to read JS files, as
// normally they are ignored as source files
"allowJs": true,
// Generate d.ts files
"declaration": true,
// This compiler run should
// only output d.ts files
"emitDeclarationOnly": true,
// Types should go into this directory.
// Removing this would place the .d.ts files
// next to the .js files
"outDir": "dist",
// go to js file when using IDE functions like
// "Go to Definition" in VSCode
"declarationMap": true
}
}
You can learn more about the options in the tsconfig reference. An alternative to using a TSConfig file is the
CLI, this is the same behavior as a CLI command.
274
Creating .d.ts Files from .js files
"main":"index.js" index.d.ts
"main":"./dist/index.js" ./dist/index.d.ts
Tips
If you'd like to write tests for your .d.ts files, try tsd.
Go to TOC
275
typescript
The type system in TypeScript has different levels of strictness when working with a codebase:
Each step represents a move towards a safer type-system, but not every project needs that level of
verification.
JSDoc annotations come before a declaration will be used to set the type of that declaration. For example:
x = 0; // OK
x = false; // OK?!
You can find the full list of supported JSDoc patterns in JSDoc Supported Types.
@ts-check
The last line of the previous code sample would raise an error in TypeScript, but it doesn't by default in a JS
project. To enable errors in your JavaScript files add: // @ts-check to the first line in your .js files to
have TypeScript raise it as an error.
// @ts-check
// @errors: 2322
/** @type {number} */
var x;
x = 0; // OK
x = false; // Not OK
If you have a lot of JavaScript files you want to add errors to then you can switch to using a jsconfig.j‐
son . You can skip checking some files by adding a // @ts-nocheck comment to files.
276
JS Projects Utilizing TypeScript
TypeScript may offer you errors which you disagree with, in those cases you can ignore errors on specific
lines by adding // @ts-ignore or // @ts-expect-error on the preceding line.
// @ts-check
/** @type {number} */
var x;
x = 0; // OK
// @ts-expect-error
x = false; // Not OK
To learn more about how JavaScript is interpreted by TypeScript read How TS Type Checks JS
Go to TOC
277
typescript
The list below outlines which constructs are currently supported when using JSDoc annotations to provide
type information in JavaScript files.
Note any tags which are not explicitly listed below (such as @async ) are not yet supported.
Types
@type
@param (or @arg or @argument )
@returns (or @return )
@typedef
@callback
@template
Classes
Documentation
@deprecated
@see
@link
Other
@enum
@author
Other supported patterns
Unsupported patterns
Unsupported tags
The meaning is usually the same, or a superset, of the meaning of the tag given at jsdoc.app. The code be-
low describes the differences and gives some example usage of each tag.
278
JSDoc Reference
Types
@type
You can reference types with the "@type" tag. The type can be:
You can use most JSDoc type syntax and any TypeScript syntax, from the most basic like string to the
most advanced, like conditional types.
/**
* @type {string}
*/
var s;
@type can specify a union type — for example, something can be either a string or a boolean.
/**
* @type {string | boolean}
*/
var sb;
You can also specify object literal types. For example, an object with properties 'a' (string) and 'b' (number)
uses the following syntax:
You can specify map-like and array-like objects using string and number index signatures, using either stan-
dard JSDoc syntax or TypeScript syntax.
279
typescript
/**
* A map-like object that maps arbitrary `string` properties to `number`s.
*
* @type {Object.<string, number>}
*/
var stringToNumber;
The preceding two types are equivalent to the TypeScript types { [x: string]: number } and { [x:
number]: any } . The compiler understands both syntaxes.
You can specify function types using either TypeScript or Google Closure syntax:
/**
* @type {*} - can be 'any' type
*/
var star;
/**
* @type {?} - unknown type (same as 'any')
*/
var question;
Casts
TypeScript borrows cast syntax from Google Closure. This lets you cast types to other types by adding a
@type tag before any parenthesized expression.
/**
* @type {number | string}
*/
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString);
280
JSDoc Reference
Import types
You can import declarations from other files using import types. This syntax is TypeScript-specific and dif-
fers from the JSDoc standard:
// @filename: types.d.ts
export type Pet = {
name: string,
};
// @filename: main.js
/**
* @param { import("./types").Pet } p
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}
// @filename: types.d.ts
export type Pet = {
name: string,
};
// @filename: main.js
// ---cut---
/**
* @typedef { import("./types").Pet } Pet
*/
/**
* @type {Pet}
*/
var myPet;
myPet.name;
import types can be used to get the type of a value from a module if you don't know the type, or if it has a
large type that is annoying to type:
// @filename: accounts.d.ts
export const userAccount = {
name: "Name",
address: "An address",
postalCode: "",
country: "",
planet: "",
system: "",
galaxy: "",
universe: "",
};
// @filename: main.js
// ---cut---
/**
* @type {typeof import("./accounts").userAccount }
*/
var x = require("./accounts").userAccount;
281
typescript
/**
* @return {PromiseLike<string>}
*/
function ps() {}
/**
* @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
*/
function ab() {}
/**
* @typedef {Object} SpecialType - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
* @prop {number} [prop4] - an optional number property of SpecialType
* @prop {number} [prop5=42] - an optional number property of SpecialType with
default
*/
/**
* @typedef {object} SpecialType1 - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
*/
282
JSDoc Reference
@param allows a similar syntax for one-off type specifications. Note that the nested property names must
be prefixed with the name of the parameter:
/**
* @param {Object} options - The shape is the same as SpecialType above
* @param {string} options.prop1
* @param {number} options.prop2
* @param {number=} options.prop3
* @param {number} [options.prop4]
* @param {number} [options.prop5=42]
*/
function special(options) {
return (options.prop4 || 1001) + options.prop5;
}
@callback is similar to @typedef , but it specifies a function type instead of an object type:
/**
* @callback Predicate
* @param {string} data
* @param {number} [index]
* @returns {boolean}
*/
Of course, any of these types can be declared using TypeScript syntax in a single-line @typedef :
@template
You can declare type parameters with the @template tag. This lets you make functions, classes, or types
that are generic:
/**
* @template T
* @param {T} x - A generic parameter that flows through to the return type
* @returns {T}
*/
function id(x) {
return x;
}
const a = id("string");
const b = id(123);
const c = id({});
283
typescript
/**
* @template T,U,V
* @template W,X
*/
You can also specify a type constraint before the type parameter name. Only the first type parameter in a
list is constrained:
/**
* @template {string} K - K must be a string or string literal
* @template {{ serious(): string }} Seriousalizable - must have a serious method
* @param {K} key
* @param {Seriousalizable} object
*/
function seriousalize(key, object) {
// ????
}
Classes
Classes can be declared as ES6 classes.
class C {
/**
* @param {number} data
*/
constructor(data) {
// property types can be inferred
this.name = "foo";
// or set explicitly
/** @type {string | null} */
this.title = null;
284
JSDoc Reference
They can also be declared as constructor functions; use @constructor along with @this for this.
Property Modifiers
@public , @private , and @protected work exactly like public , private , and protected in
TypeScript:
// @errors: 2341
// @ts-check
class Car {
constructor() {
/** @private */
this.identifier = 100;
}
printIdentifier() {
console.log(this.identifier);
}
}
@public is always implied and can be left off, but means that a property can be reached from
anywhere.
@private means that a property can only be used within the containing class.
@protected means that a property can only be used within the containing class, and all derived sub-
classes, but not on dissimilar instances of the containing class.
@readonly
The @readonly modifier ensures that a property is only ever written to during initialization.
// @errors: 2540
// @ts-check
class Car {
constructor() {
/** @readonly */
this.identifier = 100;
}
printIdentifier() {
console.log(this.identifier);
}
}
285
typescript
@override
@override works the same way as in TypeScript; use it on methods that override a method from a base
class:
export class C {
m() { }
}
class D extends C {
/** @override */
m() { }
}
@extends
When JavaScript classes extend a generic base class, there is no JavaScript syntax for passing a type argu-
ment. The @extends tag allows this:
/**
* @template T
* @extends {Set<T>}
*/
class SortableSet extends Set {
// ...
}
Note that @extends only works with classes. Currently, there is no way for a constructor function to extend
a class.
@implements
In the same way, there is no JavaScript syntax for implementing a TypeScript interface. The @implements
tag works just like in TypeScript:
@constructor
The compiler infers constructor functions based on this-property assignments, but you can make checking
stricter and suggestions better if you add a @constructor tag:
// @checkJs
// @errors: 2345 2348
/**
286
JSDoc Reference
* @constructor
* @param {number} data
*/
function C(data) {
// property types can be inferred
this.name = "foo";
// or set explicitly
/** @type {string | null} */
this.title = null;
this.initialize(data);
}
/**
* @param {string} s
*/
C.prototype.initialize = function (s) {
this.size = s.length;
};
Note: Error messages only show up in JS codebases with a JSConfig and checkJs enabled.
With @constructor , this is checked inside the constructor function C , so you will get suggestions for
the initialize method and an error if you pass it a number. Your editor may also show warnings if you
call C instead of constructing it.
Unfortunately, this means that constructor functions that are also callable cannot use @constructor .
@this
The compiler can usually figure out the type of this when it has some context to work with. When it
doesn't, you can explicitly specify the type of this with @this :
/**
* @this {HTMLElement}
* @param {*} e
*/
function callbackForLater(e) {
this.clientHeight = parseInt(e); // should be fine!
}
Documentation
@deprecated
287
typescript
When a function, method, or property is deprecated you can let users know by marking it with a /** @dep‐
recated */ JSDoc comment. That information is surfaced in completion lists and as a suggestion diagnos-
tic that editors can handle specially. In an editor like VS Code, deprecated values are typically displayed in a
strike-through style like this.
// @noErrors
/** @deprecated */
const apiV1 = {};
const apiV2 = {};
apiV;
// ^|
@see
@see lets you link to other names in your program:
type Box<T> = { t: T }
/** @see Box for implementation details */
type Boxify<T> = { [K in keyof T]: Box<T> };
Some editors will turn Box into a link to make it easy to jump there and back.
@link
@link is like @see , except that it can be used inside other tags:
type Box<T> = { t: T }
/** @returns A {@link Box} containing the parameter. */
function box<U>(u: U): Box<U> {
return { t: u };
}
Other
@enum
The @enum tag allows you to create an object literal whose members are all of a specified type. Unlike most
object literals in JavaScript, it does not allow other members. @enum is intended for compatibility with
Google Closure's @enum tag.
JSDocState.SawAsterisk;
Note that @enum is quite different from, and much simpler than, TypeScript's enum . However, unlike
TypeScript's enums, @enum can have any type:
288
JSDoc Reference
MathFuncs.add1;
@author
You can specify the author of an item with @author :
/**
* Welcome to awesome.ts
* @author Ian Awesome <[email protected]>
*/
Remember to surround the email address with angle brackets. Otherwise, @example will be parsed as a
new tag.
class Foo {}
// ---cut---
var someObj = {
/**
* @param {string} param1 - JSDocs on property assignments work
*/
x: function (param1) {},
};
/**
* As do jsdocs on variable assignments
* @return {Window}
*/
let someFunc = function () {};
/**
* And class methods
* @param {string} greeting The greeting to use
*/
Foo.prototype.sayHi = (greeting) => console.log("Hi!");
/**
* And arrow function expressions
* @param {number} x - A multiplier
*/
let myArrow = (x) => x * x;
/**
* Which means it works for function components in JSX too
* @param {{a: string, b: number}} props - Some param
*/
var fc = (props) => <div>{props.a.charAt(0)}</div>;
/**
* A parameter can be a class constructor, using Google Closure syntax.
*
289
typescript
/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn10(p1) {}
/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn9(p1) {
return p1.join();
}
Unsupported patterns
Postfix equals on a property type in an object literal type doesn't specify an optional property:
/**
* @type {{ a: string, b: number= }}
*/
var wrong;
/**
* Use postfix question on the property name instead:
* @type {{ a: string, b?: number }}
*/
var right;
/**
* @type {?number}
* With strictNullChecks: true -- number | null
* With strictNullChecks: false -- number
*/
var nullable;
/**
* @type {number | null}
* With strictNullChecks: true -- number | null
* With strictNullChecks: false -- number
*/
var unionNullable;
Non-nullable types have no meaning and are treated just as their original type:
/**
* @type {!number}
* Just has type number
*/
var normal;
290
JSDoc Reference
Unlike JSDoc's type system, TypeScript only allows you to mark types as containing null or not. There is no
explicit non-nullability -- if strictNullChecks is on, then number is not nullable. If it is off, then number is
nullable.
Unsupported tags
TypeScript ignores any unsupported JSDoc tags.
Go to TOC
291
typescript
Here are some notable differences on how checking works in .js files compared to .ts files.
In a .js file, the compiler infers properties from property assignments inside the class body. The type of a
property is the type given in the constructor, unless it's not defined there, or the type in the constructor is
undefined or null. In that case, the type is the union of the types of all the right-hand values in these as-
signments. Properties defined in the constructor are always assumed to exist, whereas ones defined just in
methods, getters, or setters are considered optional.
// @checkJs
// @errors: 2322
class C {
constructor() {
this.constructorOnly = 0;
this.constructorUnknown = undefined;
}
method() {
this.constructorOnly = false;
this.constructorUnknown = "plunkbat"; // ok, constructorUnknown is string |
undefined
this.methodOnly = "ok"; // ok, but methodOnly could also be undefined
}
method2() {
this.methodOnly = true; // also, ok, methodOnly's type is string | boolean |
undefined
}
}
If properties are never set in the class body, they are considered unknown. If your class has properties that
are only read from, add and then annotate a declaration in the constructor with JSDoc to specify the type.
You don't even have to give a value if it will be initialized later:
// @checkJs
// @errors: 2322
class C {
constructor() {
/** @type {number | undefined} */
this.prop = undefined;
/** @type {number | undefined} */
this.count;
}
}
292
Type Checking JavaScript Files
// @checkJs
// @errors: 2683 2322
function C() {
this.constructorOnly = 0;
this.constructorUnknown = undefined;
}
C.prototype.method = function () {
this.constructorOnly = false;
this.constructorUnknown = "plunkbat"; // OK, the type is string | undefined
};
The module support in JavaScript is much more syntactically forgiving than TypeScript's module support.
Most combinations of assignments and declarations are supported.
class C {}
C.D = class {};
function Outer() {
this.y = 2;
}
Outer.Inner = function () {
this.yy = 2;
};
Outer.Inner();
293
typescript
var ns = {};
ns.C = class {};
ns.func = function () {};
ns;
// IIFE
var ns = (function (n) {
return n || {};
})();
ns.CONST = 1;
// defaulting to global
var assign =
assign ||
function () {
// code goes here
};
assign.extra = 1;
var obj = { a: 1 };
obj.b = 2; // Allowed
Object literals behave as if they have an index signature [x:string]: any that allows them to be treated
as open maps instead of closed objects.
Like other special JS checking behaviors, this behavior can be changed by specifying a JSDoc type for the
variable. For example:
// @checkJs
// @errors: 2339
/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2;
294
Type Checking JavaScript Files
It is important to note that it is an error to call a function with too many arguments.
For instance:
// @checkJs
// @strict: false
// @errors: 7006 7006 2554
function bar(a, b) {
console.log(a + " " + b);
}
JSDoc annotated functions are excluded from this rule. Use JSDoc optional parameter syntax ( [ ] ) to ex-
press optionality. e.g.:
/**
* @param {string} [somebody] - Somebody's name.
*/
function sayHello(somebody) {
if (!somebody) {
somebody = "John Doe";
}
console.log("Hello " + somebody);
}
sayHello();
295
typescript
In extends clause
For instance, React.Component is defined to have two type parameters, Props and State . In a .js file,
there is no legal way to specify these in the extends clause. By default the type arguments will be any :
/**
* @augments {Component<{a: number}, State>}
*/
class MyComponent extends Component {
render() {
this.props.b; // Error: b does not exist on {a:number}
}
}
In JSDoc references
An unspecified type argument in JSDoc defaults to any:
296
Type Checking JavaScript Files
/** @type{Array} */
var x = [];
x.push(1); // OK
x.push("string"); // OK, x is of type Array<any>
/** @type{Array.<number>} */
var y = [];
y.push(1); // OK
y.push("string"); // Error, string is not assignable to number
In function calls
A call to a generic function uses the arguments to infer the type parameters. Sometimes this process fails to
infer any types, mainly because of lack of inference sources; in these cases, the type parameters will de-
fault to any . For example:
p; // Promise<any>;
Go to TOC
297
typescript
# Emit JS for any .ts files in the folder src, with the default settings
tsc src/*.ts
# Emit d.ts files for a js file with showing compiler options which are booleans
tsc index.js --declaration --emitDeclarationOnly
# Emit a single .js file from two files via compiler options which take string
arguments
tsc app.ts util.ts --target esnext --outfile index.js
Compiler Options
If you're looking for more information about the compiler options in a tsconfig, check out the
TSConfig Reference
CLI Commands
Flag Type
--all boolean
--generateTrace string
--help boolean
298
tsc CLI Options
Flag Type
--init boolean
--listFilesOnly boolean
Print names of files that are part of the compilation and then stop processing.
--locale string
Set the language of the messaging from TypeScript. This does not affect emit.
--project string
Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
--showConfig boolean
--version boolean
Build Options
Flag Type
--build boolean
--clean boolean
299
typescript
Flag Type
--dry boolean
--force boolean
--verbose boolean
Watch Options
Flag Type
--
list
excludeDirectories
--excludeFiles list
Specify what approach the watcher should use if the system runs out of native file watchers.
--
synchronousWatchDi‐ boolean
rectory
300
tsc CLI Options
Flag Type
Synchronously call callbacks and update the state of directory watchers on platforms that don`t support
recursive watching natively.
--watch boolean
Specify how directories are watched on systems that lack recursive file-watching functionality.
Compiler Flags
false otherwise.
Allow 'import x from y' when a module doesn't have a default export.
301
typescript
--allowUnreachableCode boolean
--allowUnusedLabels boolean
true if strict ,
--alwaysStrict boolean
false otherwise.
--
assumeChangesOnlyAffectDirectDepen‐ boolean false
dencies
--baseUrl string
302
tsc CLI Options
true if composite ,
--declaration boolean
false otherwise.
Generate .d.ts files from TypeScript and JavaScript files in your project.
--declarationDir string
Remove the 20mb cap on total source code size for JavaScript files in
the TypeScript language server.
303
typescript
--
disableSourceOfProjectReferenceRedi‐ boolean false
rect
Emit more compliant, but verbose and less performant JavaScript for
iteration.
Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files.
304
tsc CLI Options
Print files read during the compilation including why it was included.
Allow importing helper functions from tslib once per project, instead of
including them per-file.
remove , pre‐
--importsNotUsedAsValues serve , or er‐ remove
ror
Specify emit/checking behavior for imports that are only used for types.
305
typescript
true if composite ,
--incremental boolean
false otherwise.
Ensure that each file can be safely transpiled without relying on other
imports.
preserve , re‐
act , react-
--jsx native ,
react-jsx , or
react-jsxdev
Specify the JSX factory function used when targeting React JSX emit,
e.g. 'React.createElement' or 'h'.
306
tsc CLI Options
Specify the JSX Fragment reference used for fragments when targeting
React JSX emit e.g. 'React.Fragment' or 'Fragment'.
Specify module specifier used to import the JSX factory functions when
using jsx: react-jsx* .
--lib list
Specify a set of bundled library declaration files that describe the target
runtime environment.
--mapRoot string
Specify the location where debugger should locate map files instead of
generated locations.
--maxNodeModuleJsDepth number 0
307
typescript
Specify the maximum folder depth used for checking JavaScript files
from node_modules . Only applicable with allowJs .
none , common‐
js , amd , umd ,
system , CommonJS if target is
es6 / es2015 , ES3 or ES5 ,
--module
es2020 , es‐
2022 , esnext , ES6 / ES2015 otherwise.
node16 , or
nodenext
Classic if module is
AMD , UMD , System or
classic , ES6 / ES2015 ,
--moduleResolution node , node16 ,
Matches if module is
or nodenext
node12 or nodenext ,
Node otherwise.
--moduleSuffixes list
308
tsc CLI Options
true if strict ,
--noImplicitAny boolean
false otherwise.
309
typescript
true if strict ,
--noImplicitThis boolean
false otherwise.
--
boolean false
noPropertyAccessFromIndexSignature
310
tsc CLI Options
--out string
--outDir string
--outFile string
Specify a file that bundles all outputs into one JavaScript file. If decla‐
ration is true, also designates a file that bundles all .d.ts output.
--paths object
--plugins list
311
typescript
true if isolatedMod‐
ules ,
--preserveConstEnums boolean
false otherwise.
Specify the object invoked for createElement . This only applies when
targeting react JSX emit.
312
tsc CLI Options
Skip type checking .d.ts files that are included with TypeScript.
--sourceRoot string
Specify the root path for debuggers to find the reference source code.
313
typescript
true if strict ,
--strictBindCallApply boolean
false otherwise.
Check that the arguments for bind , call , and apply methods
match the original function.
true if strict ,
--strictFunctionTypes boolean
false otherwise.
true if strict ,
--strictNullChecks boolean
false otherwise.
true if strict ,
--strictPropertyInitialization boolean
false otherwise.
Check for class properties that are declared but not set in the
constructor.
314
tsc CLI Options
es3 , es5 ,
es6 / es2015 ,
es2016 , es‐
2017 , es2018 ,
--target ES3
es2019 , es‐
2020 , es2021 ,
es2022 , or es‐
next
Set the JavaScript language version for emitted JavaScript and include
compatible library declarations.
--typeRoots list
--
list
types
315
typescript
true if target is
ES2022 or higher, includ-
--useDefineForClassFields boolean ing ESNext ,
false otherwise.
true if strict ,
--useUnknownInCatchVariables boolean
false otherwise.
Related
Every option is fully explained in the TSConfig Reference.
Learn how to use a tsconfig.json files.
Learn how to work in an MSBuild project.
Go to TOC
316
Compiler Options in MSBuild
Overview
When you have an MSBuild based project which utilizes TypeScript such as an ASP.NET Core project, you
can configure TypeScript in two ways. Either via a tsconfig.json or via the project settings.
Using a tsconfig.json
We recommend using a tsconfig.json for your project when possible. To add one to an existing project,
add a new item to your project which is called a "TypeScript JSON Configuration File" in modern versions of
Visual Studio.
The new tsconfig.json will then be used as the source of truth for TypeScript-specific build information
like files and configuration. You can learn about how TSConfigs works here and there is a comprehensive
reference here.
<PropertyGroup>
<TypeScriptNoEmitOnError>true</TypeScriptNoEmitOnError>
<TypeScriptNoImplicitReturns>true</TypeScriptNoImplicitReturns>
</PropertyGroup>
There is a series of mappings for common TypeScript settings, these are settings which map directly to
TypeScript cli options and are used to help you write a more understandable project file. You can use the
TSConfig reference to get more information on what values and defaults are for each mapping.
CLI Mappings
MSBuild
Config TSC Flag
Name
<TypeScriptAllowJS> --allowJs
Allow JavaScript files to be a part of your program. Use the checkJS option to get errors from these
files.
<TypeScriptRemoveComments> --removeComments
<TypeScriptNoImplicitAny> --noImplicitAny
317
typescript
MSBuild
Config TSC Flag
Name
Enable error reporting for expressions and declarations with an implied any type..
<TypeScriptGeneratesDeclarations> --declaration
Generate .d.ts files from TypeScript and JavaScript files in your project.
<TypeScriptModuleKind> --module
<TypeScriptJSXEmit> --jsx
<TypeScriptOutDir> --outDir
<TypeScriptSourceMap> --sourcemap
<TypeScriptTarget> --target
Set the JavaScript language version for emitted JavaScript and include compatible library declarations.
<TypeScriptNoResolve> --noResolve
Disallow import s, require s or <reference> s from expanding the number of files TypeScript should
add to a project.
<TypeScriptMapRoot> --mapRoot
Specify the location where debugger should locate map files instead of generated locations.
<TypeScriptSourceRoot> --sourceRoot
Specify the root path for debuggers to find the reference source code.
<TypeScriptCharset> --charset
318
Compiler Options in MSBuild
MSBuild
Config TSC Flag
Name
No longer supported. In early versions, manually set the text encoding for reading files.
<TypeScriptEmitBOM> --emitBOM
Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files.
<TypeScriptNoLib> --noLib
<TypeScriptPreserveConstEnums> --preserveConstEnums
--
<TypeScriptSuppressImplicitAnyIndexErrors> suppressImplicitAnyIndexEr‐
rors
Suppress noImplicitAny errors when indexing objects that lack index signatures.
<TypeScriptNoEmitHelpers> --noEmitHelpers
<TypeScriptInlineSourceMap> --inlineSourceMap
<TypeScriptInlineSources> --inlineSources
<TypeScriptNewLine> --newLine
<TypeScriptIsolatedModules> --isolatedModules
Ensure that each file can be safely transpiled without relying on other imports.
<TypeScriptEmitDecoratorMetadata> --emitDecoratorMetadata
319
typescript
MSBuild
Config TSC Flag
Name
<TypeScriptRootDir> --rootDir
<TypeScriptExperimentalDecorators> --experimentalDecorators
<TypeScriptModuleResolution> --moduleResolution
--
<TypeScriptSuppressExcessPropertyErrors>
suppressExcessPropertyErrors
Disable reporting of excess property errors during the creation of object literals.
<TypeScriptReactNamespace> --reactNamespace
Specify the object invoked for createElement . This only applies when targeting react JSX emit.
<TypeScriptSkipDefaultLibCheck> --skipDefaultLibCheck
Skip type checking .d.ts files that are included with TypeScript.
<TypeScriptAllowUnusedLabels> --allowUnusedLabels
<TypeScriptNoImplicitReturns> --noImplicitReturns
Enable error reporting for codepaths that do not explicitly return in a function.
<TypeScriptNoFallthroughCasesInSwitch> --noFallthroughCasesInSwitch
<TypeScriptAllowUnreachableCode> --allowUnreachableCode
320
Compiler Options in MSBuild
MSBuild
Config TSC Flag
Name
--
<TypeScriptForceConsistentCasingInFile‐
forceConsistentCasingInFile‐
Names>
Names
--
<TypeScriptAllowSyntheticDefaultImports>
allowSyntheticDefaultImports
Allow 'import x from y' when a module doesn't have a default export.
<TypeScriptNoImplicitUseStrict> --noImplicitUseStrict
<TypeScriptLib> --lib
Specify a set of bundled library declaration files that describe the target runtime environment.
<TypeScriptBaseUrl> --baseUrl
<TypeScriptDeclarationDir> --declarationDir
<TypeScriptNoImplicitThis> --noImplicitThis
<TypeScriptSkipLibCheck> --skipLibCheck
<TypeScriptStrictNullChecks> --strictNullChecks
321
typescript
MSBuild
Config TSC Flag
Name
<TypeScriptNoUnusedLocals> --noUnusedLocals
<TypeScriptNoUnusedParameters> --noUnusedParameters
<TypeScriptAlwaysStrict> --alwaysStrict
<TypeScriptImportHelpers> --importHelpers
Allow importing helper functions from tslib once per project, instead of including them per-file.
<TypeScriptJSXFactory> --jsxFactory
Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'
<TypeScriptStripInternal> --stripInternal
<TypeScriptCheckJs> --checkJs
<TypeScriptDownlevelIteration> --downlevelIteration
Emit more compliant, but verbose and less performant JavaScript for iteration.
<TypeScriptStrict> --strict
<TypeScriptNoStrictGenericChecks> --noStrictGenericChecks
<TypeScriptPreserveSymlinks> --preserveSymlinks
322
Compiler Options in MSBuild
MSBuild
Config TSC Flag
Name
Disable resolving symlinks to their realpath. This correlates to the same flag in node.
<TypeScriptStrictFunctionTypes> --strictFunctionTypes
When assigning functions, check to ensure parameters and the return values are subtype-compatible.
--
<TypeScriptStrictPropertyInitialization>
strictPropertyInitialization
Check for class properties that are declared but not set in the constructor.
<TypeScriptESModuleInterop> --esModuleInterop
Emit additional JavaScript to ease support for importing CommonJS modules. This enables allowSyn‐
theticDefaultImports for type compatibility.
<TypeScriptEmitDeclarationOnly> --emitDeclarationOnly
<TypeScriptKeyofStringsOnly> --keyofStringsOnly
Make keyof only return strings instead of string, numbers or symbols. Legacy option.
<TypeScriptUseDefineForClassFields> --useDefineForClassFields
<TypeScriptDeclarationMap> --declarationMap
<TypeScriptResolveJsonModule> --resolveJsonModule
<TypeScriptStrictBindCallApply> --strictBindCallApply
Check that the arguments for bind , call , and apply methods match the original function.
<TypeScriptNoEmitOnError> --noEmitOnError
323
typescript
MSBuild
Config TSC Flag
Name
Additional Flags
Because the MSBuild system passes arguments directly to the TypeScript CLI, you can use the option
TypeScriptAdditionalFlags to provide specific flags which don't have a mapping above.
<TypeScriptAdditionalFlags> $(TypeScriptAdditionalFlags) --
noPropertyAccessFromIndexSignature</TypeScriptAdditionalFlags>
<Import
Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\
Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\T
/>
ToolsVersion
The value of <TypeScriptToolsVersion>1.7</TypeScriptToolsVersion> property in the project file
identifies the compiler version to use to build (1.7 in this example). This allows a project to build against the
same versions of the compiler on different machines.
If TypeScriptToolsVersion is not specified, the latest compiler version installed on the machine will be
used to build.
Users using newer versions of TS, will see a prompt to upgrade their project on first load.
324
Compiler Options in MSBuild
TypeScriptCompileBlocked
If you are using a different build tool to build your project (e.g. gulp, grunt , etc.) and VS for the develop-
ment and debugging experience, set <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
in your project. This should give you all the editing support, but not the build when you hit F5.
Go to TOC
325
typescript
Compiler supports configuring how to watch files and directories using compiler flags in TypeScript 3.8+,
and environment variables before that.
Background
The --watch implementation of the compiler relies on using fs.watch and fs.watchFile which are pro-
vided by node, both of these methods have pros and cons.
fs.watch uses file system events to notify the changes in the file/directory. But this is OS dependent and
the notification is not completely reliable and does not work as expected on many OS. Also there could be
limit on number of watches that can be created, e.g. linux and we could exhaust it pretty quickly with pro-
grams that include large number of files. But because this uses file system events, there is not much CPU
cycle involved. Compiler typically uses fs.watch to watch directories (e.g. source directories included by
config file, directories in which module resolution failed etc) These can handle the missing precision in noti-
fying about the changes. But recursive watching is supported on only Windows and OSX. That means we
need something to replace the recursive nature on other OS.
fs.watchFile uses polling and thus involves CPU cycles. However, fs.watchFile is the most reliable
mechanism to get the update on the status of file/directory. The compiler typically uses fs.watchFile to
watch source files, config files and missing files (missing file references). This means the CPU usage when
using fs.watchFile depends on number of files in the program.
326
Configuring Watch
327
typescript
Option Description
Go to TOC
328
Integrating with Build Tools
Babel
Install
npm install @babel/cli @babel/core @babel/preset-typescript --save-dev
.babelrc
{
"presets": ["@babel/preset-typescript"]
}
package.json
{
"scripts": {
"build": "babel --out-file bundle.js main.ts"
},
}
Browserify
Install
npm install tsify
Using API
var browserify = require("browserify");
var tsify = require("tsify");
browserify()
.add("main.ts")
.plugin("tsify", { noImplicitAny: true })
.bundle()
.pipe(process.stdout);
329
typescript
Grunt
Install
npm install grunt-ts
Basic Gruntfile.js
module.exports = function (grunt) {
grunt.initConfig({
ts: {
default: {
src: ["**/*.ts", "!node_modules/**/*.ts"],
},
},
});
grunt.loadNpmTasks("grunt-ts");
grunt.registerTask("default", ["ts"]);
};
Gulp
Install
npm install gulp-typescript
Basic gulpfile.js
var gulp = require("gulp");
var ts = require("gulp-typescript");
gulp.task("default", function () {
var tsResult = gulp.src("src/*.ts").pipe(
ts({
noImplicitAny: true,
out: "output.js",
})
);
return tsResult.js.pipe(gulp.dest("built/local"));
});
Jspm
Install
npm install -g jspm@beta
330
Integrating with Build Tools
MSBuild
Update project file to include locally installed Microsoft.TypeScript.Default.props (at the top) and
Microsoft.TypeScript.targets (at the bottom) files:
Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\
Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\T
/>
Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\
Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\T
/>
</Project>
More details about defining MSBuild compiler options: Setting Compiler Options in MSBuild projects
NuGet
Right-Click -> Manage NuGet Packages
Search for Microsoft.TypeScript.MSBuild
Hit Install
When install is complete, rebuild!
More details can be found at Package Manager Dialog and using nightly builds with NuGet
Rollup
Install
npm install @rollup/plugin-typescript --save-dev
331
typescript
Note that both typescript and tslib are peer dependencies of this plugin that need to be installed
separately.
Usage
Create a rollup.config.js configuration file and import the plugin:
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
export default {
input: 'src/index.ts',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [typescript()]
};
Svelte Compiler
Install
npm install --save-dev svelte-preprocess
Note that typescript is an optional peer dependencies of this plugin and needs to be installed separately.
tslib is not provided either.
Usage
Create a svelte.config.js configuration file and import the plugin:
// svelte.config.js
import preprocess from 'svelte-preprocess';
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess()
};
You can now specify that script blocks are written in TypeScript:
<script lang="ts">
Vite
Vite supports importing .ts files out-of-the-box. It only performs transpilation and not type checking. It
also requires that some compilerOptions have certain values. See the Vite docs for more details.
332
Integrating with Build Tools
Webpack
Install
npm install ts-loader --save-dev
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
Alternatives:
awesome-typescript-loader
Go to TOC
333
typescript
Project references are a new feature in TypeScript 3.0 that allow you to structure your TypeScript programs
into smaller pieces.
By doing this, you can greatly improve build times, enforce logical separation between components, and or-
ganize your code in new and better ways.
We're also introducing a new mode for tsc , the --build flag, that works hand in hand with project refer-
ences to enable faster TypeScript builds.
An Example Project
Let's look at a fairly normal program and see how project references can help us better organize it. Imagine
you have a project with two modules, converter and units , and a corresponding test file for each:
/
├── src/
│ ├── converter.ts
│ └── units.ts
├── test/
│ ├── converter-tests.ts
│ └── units-tests.ts
└── tsconfig.json
The test files import the implementation files and do some testing:
// converter-tests.ts
import * as converter from "../converter";
assert.areEqual(converter.celsiusToFahrenheit(0), 32);
Previously, this structure was rather awkward to work with if you used a single tsconfig file:
It was possible for the implementation files to import the test files
It wasn't possible to build test and src at the same time without having src appear in the output
folder name, which you probably don't want
Changing just the internals in the implementation files required typechecking the tests again, even
though this wouldn't ever cause new errors
Changing just the tests required typechecking the implementation again, even if nothing changed
You could use multiple tsconfig files to solve some of those problems, but new ones would appear:
There's no built-in up-to-date checking, so you end up always running tsc twice
Invoking tsc twice incurs more startup time overhead
tsc -w can't run on multiple config files at once
334
Project References
{
"compilerOptions": {
// The usual
},
"references": [
{ "path": "../src" }
]
}
The path property of each reference can point to a directory containing a tsconfig.json file, or to the
config file itself (which may have any name).
Importing modules from a referenced project will instead load its output declaration file ( .d.ts )
If the referenced project produces an outFile , the output file .d.ts file's declarations will be visible in
this project
Build mode (see below) will automatically build the referenced project if needed
By separating into multiple projects, you can greatly improve the speed of typechecking and compiling, re-
duce memory usage when using an editor, and improve enforcement of the logical groupings of your
program.
composite
Referenced projects must have the new composite setting enabled. This setting is needed to ensure
TypeScript can quickly determine where to find the outputs of the referenced project. Enabling the compos‐
ite flag changes a few things:
The rootDir setting, if not explicitly set, defaults to the directory containing the tsconfig file
All implementation files must be matched by an include pattern or listed in the files array. If this
constraint is violated, tsc will inform you which files weren't specified
declaration must be turned on
declarationMap s
We've also added support for declaration source maps. If you enable declarationMap , you'll be able to use
editor features like "Go to Definition" and Rename to transparently navigate and edit code across project
boundaries in supported editors.
335
typescript
"references": [
{ "path": "../utils", "prepend": true }
]
Prepending a project will include the project's output above the output of the current project. All output files
( .js , .d.ts , .js.map , .d.ts.map ) will be emitted correctly.
tsc will only ever use existing files on disk to do this process, so it's possible to create a project where a
correct output file can't be generated because some project's output would be present more than once in
the resulting file. For example:
A
^ ^
/ \
B C
^ ^
\ /
D
It's important in this situation to not prepend at each reference, because you'll end up with two copies of A
in the output of D - this can lead to unexpected results.
Because dependent projects make use of .d.ts files that are built from their dependencies, you'll either
have to check in certain build outputs or build a project after cloning it before you can navigate the project
in an editor without seeing spurious errors.
When using VS Code (since TS 3.7) we have a behind-the-scenes in-memory .d.ts generation process
that should be able to mitigate this, but it has some perf implications. For very large composite projects you
might want to disable this using disableSourceOfProjectReferenceRedirect option.
Additionally, to preserve compatibility with existing build workflows, tsc will not automatically build depen-
dencies unless invoked with the --build switch. Let's learn more about --build .
336
Project References
You can provide tsc -b with multiple config file paths (e.g. tsc -b src test ). Just like tsc -p , speci-
fying the config file name itself is unnecessary if it's named tsconfig.json .
tsc -b Commandline
You can specify any number of config files:
Don't worry about ordering the files you pass on the commandline - tsc will re-order them if needed so
that dependencies are always built first.
--verbose : Prints out verbose logging to explain what's going on (may be combined with any other
flag)
--dry : Shows what would be done but doesn't actually build anything
--clean : Deletes the outputs of the specified projects (may be combined with --dry )
--force : Act as if all projects are out of date
--watch : Watch mode (may not be combined with any flag except --verbose )
Caveats
Normally, tsc will produce outputs ( .js and .d.ts ) in the presence of syntax or type errors, unless
noEmitOnError is on. Doing this in an incremental build system would be very bad - if one of your out-of-
date dependencies had a new error, you'd only see it once because a subsequent build would skip building
the now up-to-date project. For this reason, tsc -b effectively acts as if noEmitOnError is enabled for all
projects.
If you check in any build outputs ( .js , .d.ts , .d.ts.map , etc.), you may need to run a --force build
after certain source control operations depending on whether your source control tool preserves timestamps
between the local copy and the remote copy.
MSBuild
If you have an msbuild project, you can enable build mode by adding
<TypeScriptBuildMode>true</TypeScriptBuildMode>
to your proj file. This will enable automatic incremental build as well as cleaning.
Note that as with tsconfig.json / -p , existing TypeScript project properties will not be respected - all
settings should be managed using your tsconfig file.
337
typescript
Some teams have set up msbuild-based workflows wherein tsconfig files have the same implicit graph or-
dering as the managed projects they are paired with. If your solution is like this, you can continue to use
msbuild with tsc -p along with project references; these are fully interoperable.
Guidance
Overall Structure
With more tsconfig.json files, you'll usually want to use Configuration file inheritance to centralize your
common compiler options. This way you can change a setting in one file rather than having to edit multiple
files.
Another good practice is to have a "solution" tsconfig.json file that simply has references to all of
your leaf-node projects and sets files to an empty array (otherwise the solution file will cause double
compilation of files). Note that starting with 3.0, it is no longer an error to have an empty files array if
you have at least one reference in a tsconfig.json file.
This presents a simple entry point; e.g. in the TypeScript repo we simply run tsc -b src to build all end-
points because we list all the subprojects in src/tsconfig.json
You can see these patterns in the TypeScript repo - see src/tsconfig_base.json , src/tsconfig.json ,
and src/tsc/tsconfig.json as key examples.
Go to TOC
338
What is a tsconfig.json
Overview
The presence of a tsconfig.json file in a directory indicates that the directory is the root of a TypeScript
project. The tsconfig.json file specifies the root files and the compiler options required to compile the
project.
JavaScript projects can use a jsconfig.json file instead, which acts almost the same but has some
JavaScript-related compiler flags enabled by default.
When input files are specified on the command line, tsconfig.json files are ignored.
Examples
Example tsconfig.json files:
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true
},
"files": [
"core.ts",
"sys.ts",
"types.ts",
"scanner.ts",
"parser.ts",
"utilities.ts",
"binder.ts",
"checker.ts",
"emitter.ts",
"program.ts",
"commandLineParser.ts",
"tsc.ts",
"diagnosticInformationMap.generated.ts"
]
}
339
typescript
{
"compilerOptions": {
"module": "system",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"outFile": "../../built/local/tsc.js",
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
TSConfig Bases
Depending on the JavaScript runtime environment which you intend to run your code in, there may be a
base configuration which you can use at github.com/tsconfig/bases. These are tsconfig.json files which
your project extends from which simplifies your tsconfig.json by handling the runtime support.
For example, if you were writing a project which uses Node.js version 12 and above, then you could use the
npm module @tsconfig/node12 :
{
"extends": "@tsconfig/node12/tsconfig.json",
"compilerOptions": {
"preserveConstEnums": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
This lets your tsconfig.json focus on the unique choices for your project, and not all of the runtime me-
chanics. There are a few tsconfig bases already, and we're hoping the community can add more for different
environments.
Recommended
Node 10
Node 12
Node 14
Node 16
Deno
React Native
Svelte
340
What is a tsconfig.json
Details
The "compilerOptions" property can be omitted, in which case the compiler's defaults are used. See our
full list of supported Compiler Options.
TSConfig Reference
To learn more about the hundreds of configuration options in the TSConfig Reference.
Schema
The tsconfig.json Schema can be found at the JSON Schema Store.
Go to TOC
341
typescript
This page lists some of the more advanced ways in which you can model types, it works in tandem with the
Utility Types doc which includes types which are included in TypeScript and available globally.
// @errors: 2339
type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;
// ---cut---
let pet = getSmallPet();
To get the same code working via property accessors, we'll need to use a type assertion:
if (fishPet.swim) {
fishPet.swim();
} else if (birdPet.fly) {
birdPet.fly();
}
This isn't the sort of code you would want in your codebase however.
It just so happens that TypeScript has something called a type guard. A type guard is some expression that
performs a runtime check that guarantees the type in some scope.
342
Advanced Types
pet is Fish is our type predicate in this example. A predicate takes the form parameterName is Type ,
where parameterName must be the name of a parameter from the current function signature.
Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if
the original type is compatible.
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
Notice that TypeScript not only knows that pet is a Fish in the if branch; it also knows that in the
else branch, you don't have a Fish , so you must have a Bird .
You may use the type guard isFish to filter an array of Fish | Bird and obtain an array of Fish :
// @errors: 2345
type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// ---cut---
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter<Fish>(isFish);
const underWater3: Fish[] = zoo.filter<Fish>((pet) => isFish(pet));
343
typescript
For a n in x expression, where n is a string literal or string literal type and x is a union type, the "true"
branch narrows to types which have an optional or required property n , and the "false" branch narrows to
types which have an optional or missing property n .
However, having to define a function to figure out if a type is a primitive is kind of a pain. Luckily, you don't
need to abstract typeof x === "number" into its own function because TypeScript will recognize it as a
type guard on its own. That means we could just write these checks inline.
344
Advanced Types
These typeof type guards are recognized in two different forms: typeof v === "typename" and typeof
v !== "typename" , where "typename" can be one of typeof operator's return values ( "undefined" ,
"number" , "string" , "boolean" , "bigint" , "symbol" , "object" , or "function" ). While
TypeScript won't stop you from comparing to other strings, the language won't recognize those expressions
as type guards.
instanceof type guards are a way of narrowing types using their constructor function. For instance, let's
borrow our industrial strength string-padder example from earlier:
interface Padder {
getPaddingString(): string;
}
function getRandomPadder() {
return Math.random() < 0.5
? new SpaceRepeatingPadder(4)
: new StringPadder(" ");
}
The right side of the instanceof needs to be a constructor function, and TypeScript will narrow down to:
1. the type of the function's prototype property if its type is not any
2. the union of types returned by that type's construct signatures
345
typescript
in that order.
Nullable types
TypeScript has two special types, null and undefined , that have the values null and undefined respec-
tively. We mentioned these briefly in the Basic Types section.
By default, the type checker considers null and undefined assignable to anything. Effectively, null and
undefined are valid values of every type. That means it's not possible to stop them from being assigned to
any type, even when you would like to prevent it. The inventor of null , Tony Hoare, calls this his "billion
dollar mistake".
The strictNullChecks flag fixes this: when you declare a variable, it doesn't automatically include null
or undefined . You can include them explicitly using a union type:
// @errors: 2322
let exampleString = "foo";
exampleString = null;
stringOrNull = undefined;
Note that TypeScript treats null and undefined differently in order to match JavaScript semantics.
string | null is a different type than string | undefined and string | undefined | null .
From TypeScript 3.7 and onwards, you can use optional chaining to simplify working with nullable types.
// @errors: 2345
function f(x: number, y?: number) {
return x + (y ?? 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null);
// @strict: false
// @strictNullChecks: true
// @errors: 2322
class C {
a: number;
b?: number;
}
346
Advanced Types
c.a = 12;
c.a = undefined;
c.b = 13;
c.b = undefined;
c.b = null;
The null elimination is pretty obvious here, but you can use terser operators too:
In cases where the compiler can't eliminate null or undefined , you can use the type assertion operator
to manually remove them. The syntax is postfix ! : identifier! removes null and undefined from
the type of identifier :
// @errors: 2532
function getUser(id: string): UserAccount | undefined {
return {} as any;
}
// ---cut---
interface UserAccount {
id: number;
email?: string;
}
if (user) {
user.email.length;
}
// Instead if you are sure that these objects or fields exist, the
// postfix ! lets you short circuit the nullability
user!.email!.length;
Type Aliases
Type aliases create a new name for a type. Type aliases are sometimes similar to interfaces, but can name
primitives, unions, tuples, and any other types that you'd otherwise have to write by hand.
347
typescript
Aliasing doesn't actually create a new type - it creates a new name to refer to that type. Aliasing a primitive
is not terribly useful, though it can be used as a form of documentation.
Just like interfaces, type aliases can also be generic - we can just add type parameters and use them on the
right side of the alias declaration:
type Tree<T> = {
value: T;
left?: Tree<T>;
right?: Tree<T>;
};
Together with intersection types, we can make some pretty mind-bending types:
interface Person {
name: string;
}
Almost all features of an interface are available in type , the key distinction is that a type cannot be re-
opened to add new properties vs an interface which is always extendable.
Interface Type
348
Advanced Types
Adding new fields to an existing interface A type cannot be changed after being created
Because an interface more closely maps how JavaScript objects work by being open to extension, we rec-
ommend using an interface over a type alias when possible.
On the other hand, if you can't express some shape with an interface and you need to use a union or tuple
type, type aliases are usually the way to go.
349
typescript
Much of the time when we talk about "singleton types", we're referring to both enum member types as well
as numeric/string literal types, though many users will use "singleton types" and "literal types"
interchangeably.
class BasicCalculator {
public constructor(protected value: number = 0) {}
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// ... other operations go here ...
}
Since the class uses this types, you can extend it and the new class can use the old methods with no
changes.
class BasicCalculator {
public constructor(protected value: number = 0) {}
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// ... other operations go here ...
}
// ---cut---
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... other operations go here ...
350
Advanced Types
Without this types, ScientificCalculator would not have been able to extend BasicCalculator and
keep the fluent interface. multiply would have returned BasicCalculator , which doesn't have the sin
method. However, with this types, multiply returns this , which is ScientificCalculator here.
Index types
With index types, you can get the compiler to check code that uses dynamic property names. For example,
a common JavaScript pattern is to pick a subset of properties from an object:
Here's how you would write and use this function in TypeScript, using the index type query and indexed
access operators:
interface Car {
manufacturer: string;
model: string;
year: number;
}
The compiler checks that manufacturer and model are actually properties on Car . The example intro-
duces a couple of new type operators. First is keyof T , the index type query operator. For any type T ,
keyof T is the union of known, public property names of T . For example:
interface Car {
manufacturer: string;
model: string;
year: number;
}
351
typescript
// ---cut---
let carProps: keyof Car;
// ^?
keyof Car is completely interchangeable with "manufacturer" | "model" | "year" . The difference is
that if you add another property to Car , say ownersAddress: string , then keyof Car will automatically
update to be "manufacturer" | "model" | "year" | "ownersAddress" . And you can use keyof in
generic contexts like pluck , where you can't possibly know the property names ahead of time. That means
the compiler will check that you pass the right set of property names to pluck :
The second operator is T[K] , the indexed access operator. Here, the type syntax reflects the expression
syntax. That means that taxi["manufacturer"] has the type Car["manufacturer"] — which in our ex-
ample is just string . However, just like index type queries, you can use T[K] in a generic context, which
is where its real power comes to life. You just have to make sure that the type variable K extends keyof
T . Here's another example with a function named getProperty .
In getProperty , o: T and propertyName: K , so that means o[propertyName]: T[K] . Once you re-
turn the T[K] result, the compiler will instantiate the actual type of the key, so the return type of get‐
Property will vary according to which property you request.
// @errors: 2345
function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName]; // o[propertyName] is of type T[K]
}
interface Car {
manufacturer: string;
model: string;
year: number;
}
let taxi: Car = {
manufacturer: "Toyota",
model: "Camry",
year: 2014,
};
// ---cut---
let manufacturer: string = getProperty(taxi, "manufacturer");
let year: number = getProperty(taxi, "year");
352
Advanced Types
interface Dictionary<T> {
[key: string]: T;
}
let keys: keyof Dictionary<number>;
// ^?
let value: Dictionary<number>["foo"];
// ^?
If you have a type with a number index signature, keyof T will just be number .
// @errors: 2339
interface Dictionary<T> {
[key: number]: T;
}
Mapped types
A common task is to take an existing type and make each of its properties optional:
interface PersonSubset {
name?: string;
age?: number;
}
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
This happens often enough in JavaScript that TypeScript provides a way to create new types based on old
types — mapped types. In a mapped type, the new type transforms each property in the old type in the
same way. For example, you can make all properties optional or of a type readonly . Here are a couple of
examples:
type Partial<T> = {
[P in keyof T]?: T[P];
};
353
typescript
// @noErrors
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Person = {
name: string;
age: number;
};
// ---cut---
type PersonPartial = Partial<Person>;
// ^?
type ReadonlyPerson = Readonly<Person>;
// ^?
Note that this syntax describes a type rather than a member. If you want to add members, you can use an
intersection type:
// This is an error!
type WrongPartialWithNewMember<T> = {
[P in keyof T]?: T[P];
newMember: boolean;
}
Let's take a look at the simplest mapped type and its parts:
The syntax resembles the syntax for index signatures with a for .. in inside. There are three parts:
In this simple example, Keys is a hard-coded list of property names and the property type is always bool‐
ean , so this mapped type is equivalent to writing:
type Flags = {
option1: boolean;
option2: boolean;
};
Real applications, however, look like Readonly or Partial above. They're based on some existing type,
and they transform the properties in some way. That's where keyof and indexed access types come in:
354
Advanced Types
type Person = {
name: string;
age: number;
};
// ---cut---
type NullablePerson = { [P in keyof Person]: Person[P] | null };
// ^?
type PartialPerson = { [P in keyof Person]?: Person[P] };
// ^?
In these examples, the properties list is keyof T and the resulting type is some variant of T[P] . This is a
good template for any general use of mapped types. That's because this kind of transformation is homo-
morphic, which means that the mapping applies only to properties of T and no others. The compiler knows
that it can copy all the existing property modifiers before adding any new ones. For example, if
Person.name was readonly, Partial<Person>.name would be readonly and optional.
// @noErrors
type Proxy<T> = {
get(): T;
set(value: T): void;
};
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
};
Note that Readonly<T> and Partial<T> are so useful, they are included in TypeScript's standard library
along with Pick and Record :
Readonly , Partial and Pick are homomorphic whereas Record is not. One clue that Record is not
homomorphic is that it doesn't take an input type to copy properties from:
355
typescript
Non-homomorphic types are essentially creating new properties, so they can't copy property modifiers from
anywhere.
Note that keyof any represents the type of any value that can be used as an index to an object. In other-
words, keyof any is currently equal to string | number | symbol .
type Proxy<T> = {
get(): T;
set(value: T): void;
};
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
};
Note that this unwrapping inference only works on homomorphic mapped types. If the mapped type is not
homomorphic you'll have to give an explicit type parameter to your unwrapping function.
Conditional Types
A conditional type selects one of two possible types based on a condition expressed as a type relationship
test:
T extends U ? X : Y
The type above means when T is assignable to U the type is X , otherwise the type is Y .
356
Advanced Types
As an example of some types that are immediately resolved, we can take a look at the following example:
declare function f<T extends boolean>(x: T): T extends true ? string : number;
Another example would be the TypeName type alias, which uses nested conditional types:
type T0 = TypeName<string>;
// ^?
type T1 = TypeName<"a">;
// ^?
type T2 = TypeName<true>;
// ^?
type T3 = TypeName<() => void>;
// ^?
type T4 = TypeName<string[]>;
// ^?
But as an example of a place where conditional types are deferred - where they stick around instead of
picking a branch - would be in the following:
interface Foo {
propA: boolean;
propB: boolean;
}
function foo<U>(x: U) {
// Has type 'U extends Foo ? string : number'
let a = f(x);
357
typescript
In the above, the variable a has a conditional type that hasn't yet chosen a branch. When another piece of
code ends up calling foo , it will substitute in U with some other type, and TypeScript will re-evaluate the
conditional type, deciding whether it can actually pick a branch.
In the meantime, we can assign a conditional type to any other target type as long as each branch of the
conditional is assignable to that target. So in our example above we were able to assign U extends Foo ?
string : number to string | number since no matter what the conditional evaluates to, it's known to be
either string or number .
Example
Example
type T1 = Boxed<string>;
// ^?
type T2 = Boxed<number[]>;
358
Advanced Types
// ^?
type T3 = Boxed<string | number[]>;
// ^?
Notice that T has the additional constraint any[] within the true branch of Boxed<T> and it is therefore
possible to refer to the element type of the array as T[number] . Also, notice how the conditional type is
distributed over the union type in the last example.
The distributive property of conditional types can conveniently be used to filter union types:
Conditional types are particularly useful when combined with mapped types:
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Part {
id: number;
359
typescript
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
type T1 = FunctionPropertyNames<Part>;
// ^?
type T2 = NonFunctionPropertyNames<Part>;
// ^?
type T3 = FunctionProperties<Part>;
// ^?
type T4 = NonFunctionProperties<Part>;
// ^?
Note, conditional types are not permitted to reference themselves recursively. For example the following is
an error.
Example
For example, the following extracts the return type of a function type:
// @noErrors
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
Conditional types can be nested to form a sequence of pattern matches that are evaluated in order:
type T0 = Unpacked<string>;
// ^?
type T1 = Unpacked<string[]>;
// ^?
type T2 = Unpacked<() => string>;
// ^?
type T3 = Unpacked<Promise<string>>;
// ^?
type T4 = Unpacked<Promise<string>[]>;
// ^?
type T5 = Unpacked<Unpacked<Promise<string>[]>>;
// ^?
360
Advanced Types
The following example demonstrates how multiple candidates for the same type variable in co-variant posi-
tions causes a union type to be inferred:
Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection
type to be inferred:
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
? U
: never;
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;
// ^?
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
// ^?
When inferring from a type with multiple call signatures (such as the type of an overloaded function), infer-
ences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not
possible to perform overload resolution based on a list of argument types.
It is not possible to use infer declarations in constraint clauses for regular type parameters:
However, much the same effect can be obtained by erasing the type variables in the constraint and instead
specifying a conditional type:
// @noErrors
type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R
? R
: any;
Go to TOC
361
typescript
Introduction
Some of the unique concepts in TypeScript describe the shape of JavaScript objects at the type level. One
example that is especially unique to TypeScript is the concept of 'declaration merging'. Understanding this
concept will give you an advantage when working with existing JavaScript. It also opens the door to more
advanced abstraction concepts.
For the purposes of this article, "declaration merging" means that the compiler merges two separate decla-
rations declared with the same name into a single definition. This merged definition has the features of both
of the original declarations. Any number of declarations can be merged; it's not limited to just two
declarations.
Basic Concepts
In TypeScript, a declaration creates entities in at least one of three groups: namespace, type, or value.
Namespace-creating declarations create a namespace, which contains names that are accessed using a dot-
ted notation. Type-creating declarations do just that: they create a type that is visible with the declared
shape and bound to the given name. Lastly, value-creating declarations create values that are visible in the
output JavaScript.
Namespace X X
Class X X
Enum X X
Interface X
Type Alias X
Function X
Variable X
Understanding what is created with each declaration will help you understand what is merged when you
perform a declaration merge.
Merging Interfaces
The simplest, and perhaps most common, type of declaration merging is interface merging. At the most ba-
sic level, the merge mechanically joins the members of both declarations into a single interface with the
same name.
interface Box {
height: number;
width: number;
}
362
Declaration Merging
interface Box {
scale: number;
}
Non-function members of the interfaces should be unique. If they are not unique, they must be of the same
type. The compiler will issue an error if the interfaces both declare a non-function member of the same
name, but of different types.
For function members, each function member of the same name is treated as describing an overload of the
same function. Of note, too, is that in the case of interface A merging with later interface A , the second
interface will have a higher precedence than the first.
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
Notice that the elements of each group maintains the same order, but the groups themselves are merged
with later overload sets ordered first.
One exception to this rule is specialized signatures. If a signature has a parameter whose type is a single
string literal type (e.g. not a union of string literals), then it will be bubbled toward the top of its merged
overload list.
interface Document {
createElement(tagName: any): Element;
}
interface Document {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
363
typescript
interface Document {
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
Merging Namespaces
Similarly to interfaces, namespaces of the same name will also merge their members. Since namespaces
create both a namespace and a value, we need to understand how both merge.
To merge the namespaces, type definitions from exported interfaces declared in each namespace are them-
selves merged, forming a single namespace with merged interface definitions inside.
To merge the namespace value, at each declaration site, if a namespace already exists with the given name,
it is further extended by taking the existing namespace and adding the exported members of the second
namespace to the first.
namespace Animals {
export class Zebra {}
}
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Dog {}
}
is equivalent to:
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
This model of namespace merging is a helpful starting place, but we also need to understand what happens
with non-exported members. Non-exported members are only visible in the original (un-merged) name-
space. This means that after merging, merged members that came from other declarations cannot see non-
exported members.
364
Declaration Merging
namespace Animal {
let haveMuscles = true;
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // Error, because haveMuscles is not accessible here
}
}
Because haveMuscles is not exported, only the animalsHaveMuscles function that shares the same un-
merged namespace can see the symbol. The doAnimalsHaveMuscles function, even though it's part of the
merged Animal namespace can not see this un-exported member.
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel {}
}
The visibility rules for merged members is the same as described in the Merging Namespaces section, so we
must export the AlbumLabel class for the merged class to see it. The end result is a class managed inside
of another class. You can also use namespaces to add more static members to an existing class.
In addition to the pattern of inner classes, you may also be familiar with the JavaScript practice of creating
a function and then extending the function further by adding properties onto the function. TypeScript uses
declaration merging to build up definitions like this in a type-safe way.
namespace buildLabel {
365
typescript
console.log(buildLabel("Sam Smith"));
enum Color {
red = 1,
green = 2,
blue = 4,
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
} else if (colorName == "white") {
return Color.red + Color.green + Color.blue;
} else if (colorName == "magenta") {
return Color.red + Color.blue;
} else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
}
Disallowed Merges
Not all merges are allowed in TypeScript. Currently, classes can not merge with other classes or with vari-
ables. For information on mimicking class merging, see the Mixins in TypeScript section.
Module Augmentation
Although JavaScript modules do not support merging, you can patch existing objects by importing and then
updating them. Let's look at a toy Observable example:
// observable.ts
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... another exercise for the reader
};
This works fine in TypeScript too, but the compiler doesn't know about Observable.prototype.map . You
can use module augmentation to tell the compiler about it:
// observable.ts
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
366
Declaration Merging
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {
// ... another exercise for the reader
};
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map((x) => x.toFixed());
The module name is resolved the same way as module specifiers in import / export . See Modules for
more information. Then the declarations in an augmentation are merged as if they were declared in the
same file as the original.
1. You can't declare new top-level declarations in the augmentation -- just patches to existing declarations.
2. Default exports also cannot be augmented, only named exports (since you need to augment an export
by its exported name, and default is a reserved word - see #14080 for details)
Global augmentation
You can also add declarations to the global scope from inside a module:
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
};
Global augmentations have the same behavior and limits as module augmentations.
Go to TOC
367
typescript
Introduction
Further Reading:
A Complete Guide to TypeScript Decorators
With the introduction of Classes in TypeScript and ES6, there now exist certain scenarios that require addi-
tional features to support annotating or modifying classes and class members. Decorators provide a way to
add both annotations and a meta-programming syntax for class declarations and members. Decorators are
a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript.
NOTE Decorators are an experimental feature that may change in future releases.
To enable experimental support for decorators, you must enable the experimentalDecorators compiler
option either on the command line or in your tsconfig.json :
Command Line:
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Decorators
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor,
property, or parameter. Decorators use the form @expression , where expression must evaluate to a
function that will be called at runtime with information about the decorated declaration.
For example, given the decorator @sealed we might write the sealed function as follows:
function sealed(target) {
// do something with 'target' ...
}
368
Decorators
Decorator Factories
If we want to customize how a decorator is applied to a declaration, we can write a decorator factory. A
Decorator Factory is simply a function that returns the expression that will be called by the decorator at
runtime.
Decorator Composition
Multiple decorators can be applied to a declaration, for example on a single line:
// @experimentalDecorators
// @noErrors
function f() {}
function g() {}
// ---cut---
@f @g x
On multiple lines:
// @experimentalDecorators
// @noErrors
function f() {}
function g() {}
// ---cut---
@f
@g
x
When multiple decorators apply to a single declaration, their evaluation is similar to function composition in
mathematics. In this model, when composing functions f and g, the resulting composite (f ∘ g)(x) is equiva-
lent to f(g(x)).
As such, the following steps are performed when evaluating multiple decorators on a single declaration in
TypeScript:
If we were to use decorator factories, we can observe this evaluation order with the following example:
369
typescript
// @experimentalDecorators
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor:
PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor:
PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
Decorator Evaluation
There is a well defined order to how decorators applied to various declarations inside of a class are applied:
1. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each in-
stance member.
2. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static
member.
3. Parameter Decorators are applied for the constructor.
4. Class Decorators are applied for the class.
Class Decorators
A Class Decorator is declared just before a class declaration. The class decorator is applied to the construc-
tor of the class and can be used to observe, modify, or replace a class definition. A class decorator cannot
be used in a declaration file, or in any other ambient context (such as on a declare class).
The expression for the class decorator will be called as a function at runtime, with the constructor of the
decorated class as its only argument.
If the class decorator returns a value, it will replace the class declaration with the provided constructor
function.
370
Decorators
NOTE Should you choose to return a new constructor function, you must take care to maintain the
original prototype. The logic that applies decorators at runtime will not do this for you.
// @experimentalDecorators
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
// ---cut---
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
We can define the @sealed decorator using the following function declaration:
When @sealed is executed, it will seal both the constructor and its prototype, and will therefore prevent
any further functionality from being added to or removed from this class during runtime by accessing
BugReport.prototype or by defining properties on BugReport itself (note that ES2015 classes are really
just syntactic sugar to prototype-based constructor functions). This decorator does not prevent classes
from sub-classing BugReport .
Next we have an example of how to override the constructor to set new defaults.
// @errors: 2339
// @experimentalDecorators
function reportableClassDecorator<T extends { new (...args: any[]): {} }>
(constructor: T) {
return class extends constructor {
reportingURL = "http://www...";
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
371
typescript
// Note that the decorator _does not_ change the TypeScript type
// and so the new property `reportingURL` is not known
// to the type system:
bug.reportingURL;
Method Decorators
A Method Decorator is declared just before a method declaration. The decorator is applied to the Property
Descriptor for the method, and can be used to observe, modify, or replace a method definition. A method
decorator cannot be used in a declaration file, on an overload, or in any other ambient context (such as in a
declare class).
The expression for the method decorator will be called as a function at runtime, with the following three
arguments:
1. Either the constructor function of the class for a static member, or the prototype of the class for an in-
stance member.
2. The name of the member.
3. The Property Descriptor for the member.
NOTE The Property Descriptor will be undefined if your script target is less than ES5 .
If the method decorator returns a value, it will be used as the Property Descriptor for the method.
NOTE The return value is ignored if your script target is less than ES5 .
The following is an example of a method decorator ( @enumerable ) applied to a method on the Greeter
class:
// @experimentalDecorators
function enumerable(value: boolean) {
return function (target: any,propertyKey: string,descriptor: PropertyDescriptor)
{
descriptor.enumerable = value;
};
}
// ---cut---
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
372
Decorators
We can define the @enumerable decorator using the following function declaration:
The @enumerable(false) decorator here is a decorator factory. When the @enumerable(false) decorator
is called, it modifies the enumerable property of the property descriptor.
Accessor Decorators
An Accessor Decorator is declared just before an accessor declaration. The accessor decorator is applied to
the Property Descriptor for the accessor and can be used to observe, modify, or replace an accessor's defini-
tions. An accessor decorator cannot be used in a declaration file, or in any other ambient context (such as in
a declare class).
NOTE TypeScript disallows decorating both the get and set accessor for a single member.
Instead, all decorators for the member must be applied to the first accessor specified in document or-
der. This is because decorators apply to a Property Descriptor, which combines both the get and se
t accessor, not each declaration separately.
The expression for the accessor decorator will be called as a function at runtime, with the following three
arguments:
1. Either the constructor function of the class for a static member, or the prototype of the class for an in-
stance member.
2. The name of the member.
3. The Property Descriptor for the member.
NOTE The Property Descriptor will be undefined if your script target is less than ES5 .
If the accessor decorator returns a value, it will be used as the Property Descriptor for the member.
NOTE The return value is ignored if your script target is less than ES5 .
373
typescript
The following is an example of an accessor decorator ( @configurable ) applied to a member of the Point
class:
// @experimentalDecorators
function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}
// ---cut---
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
We can define the @configurable decorator using the following function declaration:
Property Decorators
A Property Decorator is declared just before a property declaration. A property decorator cannot be used in
a declaration file, or in any other ambient context (such as in a declare class).
The expression for the property decorator will be called as a function at runtime, with the following two
arguments:
1. Either the constructor function of the class for a static member, or the prototype of the class for an in-
stance member.
2. The name of the member.
374
Decorators
NOTE A Property Descriptor is not provided as an argument to a property decorator due to how
property decorators are initialized in TypeScript. This is because there is currently no mechanism to
describe an instance property when defining members of a prototype, and no way to observe or modi-
fy the initializer for a property. The return value is ignored too. As such, a property decorator can only
be used to observe that a property of a specific name has been declared for a class.
We can use this information to record metadata about the property, as in the following example:
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
We can then define the @format decorator and getFormat functions using the following function
declarations:
import "reflect-metadata";
The @format("Hello, %s") decorator here is a decorator factory. When @format("Hello, %s") is called,
it adds a metadata entry for the property using the Reflect.metadata function from the reflect-meta‐
data library. When getFormat is called, it reads the metadata value for the format.
NOTE This example requires the reflect-metadata library. See Metadata for more information
about the reflect-metadata library.
375
typescript
Parameter Decorators
A Parameter Decorator is declared just before a parameter declaration. The parameter decorator is applied
to the function for a class constructor or method declaration. A parameter decorator cannot be used in a de-
claration file, an overload, or in any other ambient context (such as in a declare class).
The expression for the parameter decorator will be called as a function at runtime, with the following three
arguments:
1. Either the constructor function of the class for a static member, or the prototype of the class for an in-
stance member.
2. The name of the member.
3. The ordinal index of the parameter in the function's parameter list.
NOTE A parameter decorator can only be used to observe that a parameter has been declared on a
method.
// @experimentalDecorators
function validate(target: any, propertyName: string, descriptor:
TypedPropertyDescriptor<any>) {}
function required(target: Object, propertyKey: string | symbol, parameterIndex:
number) {}
// ---cut---
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
@validate
print(@required verbose: boolean) {
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}
We can then define the @required and @validate decorators using the following function declarations:
// @experimentalDecorators
// @emitDecoratorMetadata
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
376
Decorators
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey,
target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] ===
undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
The @required decorator adds a metadata entry that marks the parameter as required. The @validate
decorator then wraps the existing greet method in a function that validates the arguments before invoking
the original method.
NOTE This example requires the reflect-metadata library. See Metadata for more information
about the reflect-metadata library.
Metadata
Some examples use the reflect-metadata library which adds a polyfill for an experimental metadata API.
This library is not yet part of the ECMAScript (JavaScript) standard. However, once decorators are officially
adopted as part of the ECMAScript standard these extensions will be proposed for adoption.
TypeScript includes experimental support for emitting certain types of metadata for declarations that have
decorators. To enable this experimental support, you must set the emitDecoratorMetadata compiler op-
tion either on the command line or in your tsconfig.json :
Command Line:
377
typescript
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
When enabled, as long as the reflect-metadata library has been imported, additional design-time type
information will be exposed at runtime.
// @emitDecoratorMetadata
// @experimentalDecorators
// @strictPropertyInitialization: false
import "reflect-metadata";
class Point {
constructor(public x: number, public y: number) {}
}
class Line {
private _start: Point;
private _end: Point;
@validate
set start(value: Point) {
this._start = value;
}
get start() {
return this._start;
}
@validate
set end(value: Point) {
this._end = value;
}
get end() {
return this._end;
}
}
set.call(this, value);
};
378
Decorators
// @ts-ignore
// line.end = {}
The TypeScript compiler will inject design-time type information using the @Reflect.metadata decorator.
You could consider it the equivalent of the following TypeScript:
class Line {
private _start: Point;
private _end: Point;
@validate
@Reflect.metadata("design:type", Point)
set start(value: Point) {
this._start = value;
}
get start() {
return this._start;
}
@validate
@Reflect.metadata("design:type", Point)
set end(value: Point) {
this._end = value;
}
get end() {
return this._end;
}
}
NOTE Decorator metadata is an experimental feature and may introduce breaking changes in future
releases.
Go to TOC
379
typescript
For the last few years, Node.js has been working to support running ECMAScript modules (ESM). This has
been a very difficult feature to support, since the foundation of the Node.js ecosystem is built on a different
module system called CommonJS (CJS).
Interoperating between the two module systems brings large challenges, with many new features to juggle;
however, support for ESM in Node.js is now implemented in Node.js, and the dust has begun to settle.
That's why TypeScript brings two new module and moduleResolution settings: node16 and nodenext .
{
"compilerOptions": {
"module": "nodenext",
}
}
These new modes bring a few high-level features which we'll explore here.
{
"name": "my-package",
"type": "module",
"//": "...",
"dependencies": {
}
}
This setting controls whether .js files are interpreted as ES modules or CommonJS modules, and defaults
to CommonJS when not set. When a file is considered an ES module, a few different rules come into play
compared to CommonJS:
To overlay the way TypeScript works in this system, .ts and .tsx files now work the same way. When
TypeScript finds a .ts , .tsx , .js , or .jsx file, it will walk up looking for a package.json to see
whether that file is an ES module, and use that to determine:
380
ECMAScript Modules in Node.js
When a .ts file is compiled as an ES module, ECMAScript import / export syntax is left alone in the .js
output; when it's compiled as a CommonJS module, it will produce the same output you get today under
module : commonjs .
This also means paths resolve differently between .ts files that are ES modules and ones that are CJS
modules. For example, let's say you have the following code today:
// ./foo.ts
export function helper() {
// ...
}
// ./bar.ts
import { helper } from "./foo"; // only works in CJS
helper();
This code works in CommonJS modules, but will fail in ES modules because relative import paths need to
use extensions. As a result, it will have to be rewritten to use the extension of the output of foo.ts - so
bar.ts will instead have to import from ./foo.js .
// ./bar.ts
import { helper } from "./foo.js"; // works in ESM & CJS
helper();
This might feel a bit cumbersome at first, but TypeScript tooling like auto-imports and path completion will
typically just do this for you.
One other thing to mention is the fact that this applies to .d.ts files too. When TypeScript finds a .d.ts
file in package, whether it is treated as an ESM or CommonJS file is based on the containing package.
Node.js supports two extensions to help with this: .mjs and .cjs . .mjs files are always ES modules,
and .cjs files are always CommonJS modules, and there's no way to override these.
In turn, TypeScript supports two new source file extensions: .mts and .cts . When TypeScript emits these
to JavaScript files, it will emit them to .mjs and .cjs respectively.
Furthermore, TypeScript also supports two new declaration file extensions: .d.mts and .d.cts . When
TypeScript generates declaration files for .mts and .cts , their corresponding extensions will be .d.mts
and .d.cts .
Using these extensions is entirely optional, but will often be useful even if you choose not to use them as
part of your primary workflow.
381
typescript
CommonJS Interop
Node.js allows ES modules to import CommonJS modules as if they were ES modules with a default export.
// @module: nodenext
// @filename: helper.cts
export function helper() {
console.log("hello world!");
}
// @filename: index.mts
import foo from "./helper.cjs";
In some cases, Node.js also synthesizes named exports from CommonJS modules, which can be more con-
venient. In these cases, ES modules can use a "namespace-style" import (i.e. import * as foo from
"..." ), or named imports (i.e. import { helper } from "..." ).
// @module: nodenext
// @filename: helper.cts
export function helper() {
console.log("hello world!");
}
// @filename: index.mts
import { helper } from "./helper.cjs";
There isn't always a way for TypeScript to know whether these named imports will be synthesized, but
TypeScript will err on being permissive and use some heuristics when importing from a file that is definitely
a CommonJS module.
In a CommonJS module, this just boils down to a require() call, and in an ES module, this imports cre‐
ateRequire to achieve the same thing. This will make code less portable on runtimes like the browser
(which don't support require() ), but will often be useful for interoperability. In turn, you can write the
above example using this syntax as follows:
// @module: nodenext
// @filename: helper.cts
export function helper() {
console.log("hello world!");
}
// @filename: index.mts
import foo = require("./foo.cjs");
foo.helper()
382
ECMAScript Modules in Node.js
Finally, it's worth noting that the only way to import ESM files from a CJS module is using dynamic
import() calls. This can present challenges, but is the behavior in Node.js today.
Here's an package.json that supports separate entry-points for CommonJS and ESM:
// package.json
{
"name": "my-package",
"type": "module",
"exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import": "./esm/index.js",
There's a lot to this feature, which you can read more about on the Node.js documentation. Here we'll try to
focus on how TypeScript supports it.
With TypeScript's original Node support, it would look for a "main" field, and then look for declaration files
that corresponded to that entry. For example, if "main" pointed to ./lib/index.js , TypeScript would
look for a file called ./lib/index.d.ts . A package author could override this by specifying a separate field
called "types" (e.g. "types": "./types/index.d.ts" ).
The new support works similarly with import conditions. By default, TypeScript overlays the same rules with
import conditions - if you write an import from an ES module, it will look up the import field, and from a
CommonJS module, it will look at the require field. If it finds them, it will look for a colocated declaration
file. If you need to point to a different location for your type declarations, you can add a "types" import
condition.
// package.json
{
"name": "my-package",
"type": "module",
"exports": {
".": {
// Entry-point for TypeScript resolution - must occur first!
"types": "./types/index.d.ts",
383
typescript
TypeScript also supports the "imports" field of package.json in a similar manner (looking for declaration
files alongside corresponding files), and supports packages self-referencing themselves. These features are
generally not as involved, but are supported.
Go to TOC
384
Enums
Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.
Enums allow a developer to define a set of named constants. Using enums can make it easier to document
intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.
Numeric enums
We'll first start off with numeric enums, which are probably more familiar if you're coming from other lan-
guages. An enum can be defined using the enum keyword.
enum Direction {
Up = 1,
Down,
Left,
Right,
}
Above, we have a numeric enum where Up is initialized with 1 . All of the following members are auto-in-
cremented from that point on. In other words, Direction.Up has the value 1 , Down has 2 , Left has
3 , and Right has 4 .
enum Direction {
Up,
Down,
Left,
Right,
}
Here, Up would have the value 0 , Down would have 1 , etc. This auto-incrementing behavior is useful for
cases where we might not care about the member values themselves, but do care that each value is distinct
from other values in the same enum.
Using an enum is simple: just access any member as a property off of the enum itself, and declare types
using the name of the enum:
enum UserResponse {
No = 0,
Yes = 1,
}
Numeric enums can be mixed in computed and constant members (see below). The short story is, enums
without initializers either need to be first, or have to come after numeric enums initialized with numeric con-
stants or other constant enum members. In other words, the following isn't allowed:
385
typescript
// @errors: 1061
const getSomeValue = () => 23;
// ---cut---
enum E {
A = getSomeValue(),
B,
}
String enums
String enums are a similar concept, but have some subtle runtime differences as documented below. In a
string enum, each member has to be constant-initialized with a string literal, or with another string enum
member.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
While string enums don't have auto-incrementing behavior, string enums have the benefit that they "serial-
ize" well. In other words, if you were debugging and had to read the runtime value of a numeric enum, the
value is often opaque - it doesn't convey any useful meaning on its own (though reverse mapping can often
help). String enums allow you to give a meaningful and readable value when your code runs, independent
of the name of the enum member itself.
Heterogeneous enums
Technically enums can be mixed with string and numeric members, but it's not clear why you would ever
want to do so:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
Unless you're really trying to take advantage of JavaScript's runtime behavior in a clever way, it's advised
that you don't do this.
It is the first member in the enum and it has no initializer, in which case it's assigned the value 0 :
// E.X is constant:
enum E {
X,
}
386
Enums
It does not have an initializer and the preceding enum member was a numeric constant. In this case the
value of the current enum member will be the value of the preceding enum member plus one.
enum E1 {
X,
Y,
Z,
}
enum E2 {
A = 1,
B,
C,
}
The enum member is initialized with a constant enum expression. A constant enum expression is a sub-
set of TypeScript expressions that can be fully evaluated at compile time. An expression is a constant
enum expression if it is:
It is a compile time error for constant enum expressions to be evaluated to NaN or Infinity .
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length,
}
When all members in an enum have literal enum values, some special semantics come into play.
387
typescript
The first is that enum members also become types as well! For example, we can say that certain members
can only have the value of an enum member:
// @errors: 2322
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
radius: 100,
};
The other change is that enum types themselves effectively become a union of each enum member. With
union enums, the type system is able to leverage the fact that it knows the exact set of values that exist in
the enum itself. Because of that, TypeScript can catch bugs where we might be comparing values
incorrectly. For example:
// @errors: 2367
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
//
}
}
In that example, we first checked whether x was not E.Foo . If that check succeeds, then our || will
short-circuit, and the body of the 'if' will run. However, if the check didn't succeed, then x can only be
E.Foo , so it doesn't make sense to see whether it's equal to E.Bar .
Enums at runtime
Enums are real objects that exist at runtime. For example, the following enum
enum E {
X,
Y,
Z,
}
388
Enums
enum E {
X,
Y,
Z,
}
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* This is equivalent to:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
Reverse mappings
In addition to creating an object with property names for members, numeric enums members also get a re-
verse mapping from enum values to enum names. For example, in this example:
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
389
typescript
// @showEmit
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
In this generated code, an enum is compiled into an object that stores both forward ( name -> value ) and
reverse ( value -> name ) mappings. References to other enum members are always emitted as property
accesses and never inlined.
Keep in mind that string enum members do not get a reverse mapping generated at all.
const enums
In most cases, enums are a perfectly valid solution. However sometimes requirements are tighter. To avoid
paying the cost of extra generated code and additional indirection when accessing enum values, it's possible
to use const enums. Const enums are defined using the const modifier on our enums:
Const enums can only use constant enum expressions and unlike regular enums they are completely re-
moved during compilation. Const enum members are inlined at use sites. This is possible since const enums
cannot have computed members.
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
// @showEmit
const enum Direction {
Up,
Down,
Left,
Right,
}
390
Enums
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
Inlining enum values is straightforward at first, but comes with subtle implications. These pitfalls pertain to
ambient const enums only (basically const enums in .d.ts files) and sharing them between projects, but if
you are publishing or consuming .d.ts files, these pitfalls likely apply to you, because tsc --declara‐
tion transforms .ts files into .d.ts files.
1. For the reasons laid out in the isolatedModules documentation, that mode is fundamentally incompati-
ble with ambient const enums. This means if you publish ambient const enums, downstream consumers
will not be able to use isolatedModules and those enum values at the same time.
2. You can easily inline values from version A of a dependency at compile time, and import version B at
runtime. Version A and B's enums can have different values, if you are not very careful, resulting in sur-
prising bugs, like taking the wrong branches of if statements. These bugs are especially pernicious be-
cause it is common to run automated tests at roughly the same time as projects are built, with the same
dependency versions, which misses these bugs completely.
3. importsNotUsedAsValues: "preserve" will not elide imports for const enums used as values, but am-
bient const enums do not guarantee that runtime .js files exist. The unresolvable imports cause errors
at runtime. The usual way to unambiguously elide imports, type-only imports, does not allow const enum
values, currently.
A. Do not use const enums at all. You can easily ban const enums with the help of a linter. Obviously this
avoids any issues with const enums, but prevents your project from inlining its own enums. Unlike inlining
enums from other projects, inlining a project's own enums is not problematic and has performance implica-
tions. B. Do not publish ambient const enums, by deconstifying them with the help of preserveConst‐
Enums . This is the approach taken internally by the TypeScript project itself. preserveConstEnums emits
the same JavaScript for const enums as plain enums. You can then safely strip the const modifier from
.d.ts files in a build step.
This way downstream consumers will not inline enums from your project, avoiding the pitfalls above, but a
project can still inline its own enums, unlike banning const enums entirely.
Ambient enums
Ambient enums are used to describe the shape of already existing enum types.
391
typescript
One important difference between ambient and non-ambient enums is that, in regular enums, members
that don't have an initializer will be considered constant if its preceding enum member is considered con-
stant. By contrast, an ambient (and non-const) enum member that does not have an initializer is always
considered computed.
Objects vs Enums
In modern TypeScript, you may not need an enum when an object with as const could suffice:
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
EDirection.Up;
// ^?
ODirection.Up;
// ^?
walk(EDirection.Left);
run(ODirection.Right);
The biggest argument in favour of this format over TypeScript's enum is that it keeps your codebase aligned
with the state of JavaScript, and when/if enums are added to JavaScript then you can move to the addition-
al syntax.
Go to TOC
392
Iterators and Generators
Iterables
An object is deemed iterable if it has an implementation for the Symbol.iterator property. Some built-in
types like Array , Map , Set , String , Int32Array , Uint32Array , etc. have their Symbol.iterator
property already implemented. Symbol.iterator function on an object is responsible for returning the list
of values to iterate on.
Iterable interface
Iterable is a type we can use if we want to take in types listed above which are iterable. Here is an
example:
for..of statements
for..of loops over an iterable object, invoking the Symbol.iterator property on the object. Here is a
simple for..of loop on an array:
Another distinction is that for..in operates on any object; it serves as a way to inspect properties on this
object. for..of on the other hand, is mainly interested in values of iterable objects. Built-in objects like
Map and Set implement Symbol.iterator property allowing access to stored values.
393
typescript
Code generation
Targeting ES5 and ES3
When targeting an ES5 or ES3-compliant engine, iterators are only allowed on values of Array type. It is
an error to use for..of loops on non-Array values, even if these non-Array values implement the
Symbol.iterator property.
The compiler will generate a simple for loop for a for..of loop, for instance:
When targeting an ECMAScipt 2015-compliant engine, the compiler will generate for..of loops to target
the built-in iterator implementation in the engine.
Go to TOC
394
JSX
JSX is an embeddable XML-like syntax. It is meant to be transformed into valid JavaScript, though the se-
mantics of that transformation are implementation-specific. JSX rose to popularity with the React
framework, but has since seen other implementations as well. TypeScript supports embedding, type check-
ing, and compiling JSX directly to JavaScript.
Basic usage
In order to use JSX you must do two things.
TypeScript ships with three JSX modes: preserve , react , and react-native . These modes only affect
the emit stage - type checking is unaffected. The preserve mode will keep the JSX as part of the output to
be further consumed by another transform step (e.g. Babel). Additionally the output will have a .jsx file
extension. The react mode will emit React.createElement , does not need to go through a JSX transfor-
mation before use, and the output will have a .js file extension. The react-native mode is the equiva-
lent of preserve in that it keeps all JSX, but the output will instead have a .js file extension.
Output File
Mode Input Output
Extension
<div
preserve <div /> .jsx
/>
<div
react React.createElement("div") .js
/>
react- <div
<div /> .js
native />
<div
react-jsx _jsx("div", {}, void 0); .js
/>
You can specify this mode using either the jsx command line flag or the corresponding option jsx in your
tsconfig.json file.
*Note: You can specify the JSX factory function to use when targeting react JSX emit with jsxFactor
y option (defaults to React.createElement )
395
typescript
The as operator
Recall how to write a type assertion:
This asserts the variable bar to have the type foo . Since TypeScript also uses angle brackets for type as-
sertions, combining it with JSX's syntax would introduce certain parsing difficulties. As a result, TypeScript
disallows angle bracket type assertions in .tsx files.
Since the above syntax cannot be used in .tsx files, an alternate type assertion operator should be used:
as . The example can easily be rewritten with the as operator.
The as operator is available in both .ts and .tsx files, and is identical in behavior to the angle-bracket
type assertion style.
Type Checking
In order to understand type checking with JSX, you must first understand the difference between intrinsic
elements and value-based elements. Given a JSX expression <expr /> , expr may either refer to some-
thing intrinsic to the environment (e.g. a div or span in a DOM environment) or to a custom component
that you've created. This is important for two reasons:
1. For React, intrinsic elements are emitted as strings ( React.createElement("div") ), whereas a compo-
nent you've created is not ( React.createElement(MyComponent) ).
2. The types of the attributes being passed in the JSX element should be looked up differently. Intrinsic ele-
ment attributes should be known intrinsically whereas components will likely want to specify their own
set of attributes.
TypeScript uses the same convention that React does for distinguishing between these. An intrinsic element
always begins with a lowercase letter, and a value-based element always begins with an uppercase letter.
Intrinsic elements
Intrinsic elements are looked up on the special interface JSX.IntrinsicElements . By default, if this inter-
face is not specified, then anything goes and intrinsic elements will not be type checked. However, if this in-
terface is present, then the name of the intrinsic element is looked up as a property on the
JSX.IntrinsicElements interface. For example:
396
JSX
<foo />; // ok
<bar />; // error
In the above example, <foo /> will work fine but <bar /> will result in an error since it has not been
specified on JSX.IntrinsicElements .
Note: You can also specify a catch-all string indexer on JSX.IntrinsicElements as follows:
Value-based elements
Value-based elements are simply looked up by identifiers that are in scope.
<MyComponent />; // ok
<SomeOtherComponent />; // error
Because these two types of value-based elements are indistinguishable from each other in a JSX
expression, first TS tries to resolve the expression as a Function Component using overload resolution. If
the process succeeds, then TS finishes resolving the expression to its declaration. If the value fails to re-
solve as a Function Component, TS will then try to resolve it as a class component. If that fails, TS will re-
port an error.
Function Component
As the name suggests, the component is defined as a JavaScript function where its first argument is a
props object. TS enforces that its return type must be assignable to JSX.Element .
interface FooProp {
name: string;
X: number;
Y: number;
}
397
typescript
Because a Function Component is simply a JavaScript function, function overloads may be used here as
well:
// @noErrors
declare module JSX {
interface Element {}
interface IntrinsicElements {
[s: string]: any;
}
}
// ---cut---
interface ClickableProps {
children: JSX.Element[] | JSX.Element;
}
Note: Function Components were formerly known as Stateless Function Components (SFC). As
Function Components can no longer be considered stateless in recent versions of react, the type SFC
and its alias StatelessComponent were deprecated.
Class Component
It is possible to define the type of a class component. However, to do so it is best to understand two new
terms: the element class type and the element instance type.
Given <Expr /> , the element class type is the type of Expr . So in the example above, if MyComponent
was an ES6 class the class type would be that class's constructor and statics. If MyComponent was a factory
function, the class type would be that function.
Once the class type is established, the instance type is determined by the union of the return types of the
class type's construct or call signatures (whichever is present). So again, in the case of an ES6 class, the in-
stance type would be the type of an instance of that class, and in the case of a factory function, it would be
the type of the value returned from the function.
398
JSX
class MyComponent {
render() {}
}
function MyFactoryFunction() {
return {
render: () => {},
};
}
The element instance type is interesting because it must be assignable to JSX.ElementClass or it will re-
sult in an error. By default JSX.ElementClass is {} , but it can be augmented to limit the use of JSX to
only those types that conform to the proper interface.
class MyComponent {
render() {}
}
function MyFactoryFunction() {
return { render: () => {} };
}
<MyComponent />; // ok
<MyFactoryFunction />; // ok
class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
399
typescript
For value-based elements, it is a bit more complex. It is determined by the type of a property on the ele-
ment instance type that was previously determined. Which property to use is determined by
JSX.ElementAttributesProperty . It should be declared with a single property. The name of that property
is then used. As of TypeScript 2.8, if JSX.ElementAttributesProperty is not provided, the type of first
parameter of the class element's constructor or Function Component's call will be used instead.
class MyComponent {
// specify the property on the element instance type
props: {
foo?: string;
};
}
The element attribute type is used to type check the attributes in the JSX. Optional and required properties
are supported.
Note: If an attribute name is not a valid JS identifier (like a data-* attribute), it is not considered to
be an error if it is not found in the element attributes type.
400
JSX
Additionally, the JSX.IntrinsicAttributes interface can be used to specify extra properties used by the
JSX framework which are not generally used by the components' props or arguments - for instance key in
React. Specializing further, the generic JSX.IntrinsicClassAttributes<T> type may also be used to
specify the same kind of extra attributes just for class components (and not Function Components). In this
type, the generic parameter corresponds to the class instance type. In React, this is used to allow the ref
attribute of type Ref<T> . Generally speaking, all of the properties on these interfaces should be optional,
unless you intend that users of your JSX framework need to provide some attribute on every tag.
<div>
<h1>Hello</h1>
</div>;
<div>
<h1>Hello</h1>
World
</div>;
You can specify the type of children like any other attribute. This will override the default type from, e.g. the
React typings if you use them.
interface PropsType {
children: JSX.Element
name: string
}
401
typescript
// OK
<Component name="foo">
<h1>Hello World</h1>
</Component>
Embedding Expressions
JSX allows you to embed expressions between tags by surrounding the expressions with curly braces ( {
} ).
const a = (
<div>
{["foo", "bar"].map((i) => (
<span>{i / 2}</span>
))}
</div>
);
The above code will result in an error since you cannot divide a string by a number. The output, when using
the preserve option, looks like:
const a = (
<div>
{["foo", "bar"].map(function (i) {
return <span>{i / 2}</span>;
})}
</div>
);
402
JSX
React integration
To use JSX with React you should use the React typings. These typings define the JSX namespace appro-
priately for use with React.
interface Props {
foo: string;
}
Configuring JSX
There are multiple compiler flags which can be used to customize your JSX, which work as both a compiler
flag and via inline per-file pragmas. To learn more see their tsconfig reference pages:
jsxFactory
jsxFragmentFactory
jsxImportSource
Go to TOC
403
typescript
Along with traditional OO hierarchies, another popular way of building up classes from reusable components
is to build them by combining simpler partial classes. You may be familiar with the idea of mixins or traits
for languages like Scala, and the pattern has also reached some popularity in the JavaScript community.
To get started, we'll need a class which will have the mixins applied on top of:
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
Then you need a type and a factory function which returns a class expression extending the base class.
setScale(scale: number) {
this._scale = scale;
}
With these all set up, then you can create a class which represents the base class with mixins applied:
class Sprite {
name = "";
x = 0;
y = 0;
404
Mixins
constructor(name: string) {
this.name = name;
}
}
type Constructor = new (...args: any[]) => {};
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;
setScale(scale: number) {
this._scale = scale;
}
Constrained Mixins
In the above form, the mixin's have no underlying knowledge of the class which can make it hard to create
the design you want.
To model this, we modify the original constructor type to accept a generic argument.
This allows for creating classes which only work with constrained base classes:
constructor(name: string) {
this.name = name;
}
}
// ---cut---
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
405
typescript
Then you can create mixins which only work when you have a particular base to build on:
constructor(name: string) {
this.name = name;
}
}
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
// ---cut---
Alternative Pattern
Previous versions of this document recommended a way to write mixins where you created both the runtime
and type hierarchies separately, then merged them at the end:
// @strict: false
// Each mixin is a traditional ES class
class Jumpable {
jump() {}
}
class Duckable {
duck() {}
}
406
Mixins
This pattern relies less on the compiler, and more on your codebase to ensure both runtime and type-sys-
tem are correctly kept in sync.
Constraints
The mixin pattern is supported natively inside the TypeScript compiler by code flow analysis. There are a
few cases where you can hit the edges of the native support.
You cannot use decorators to provide mixins via code flow analysis:
// @experimentalDecorators
// @errors: 2339
// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
return class Pausable extends target {
shouldFreeze = false;
};
};
@Pausable
class Player {
x = 0;
y = 0;
}
// The Player class does not have the decorator's type merged:
const player = new Player();
player.shouldFreeze;
More of a gotcha than a constraint. The class expression pattern creates singletons, so they can't be
mapped at the type system to support different variable types.
407
typescript
You can work around this by using functions to return your classes which differ based on a generic:
function base<T>() {
class Base {
static prop: T;
}
return Base;
}
function derived<T>() {
class Derived extends base<T>() {
static anotherProp: T;
}
return Derived;
}
Spec.prop; // string
Spec.anotherProp; // string
Go to TOC
408
Module Resolution
This section assumes some basic knowledge about modules. Please see the Modules documentation
for more information.
Module resolution is the process the compiler uses to figure out what an import refers to. Consider an im-
port statement like import { a } from "moduleA" ; in order to check any use of a , the compiler needs
to know exactly what it represents, and will need to check its definition moduleA .
At this point, the compiler will ask "what's the shape of moduleA ?" While this sounds straightforward, mod‐
uleA could be defined in one of your own .ts / .tsx files, or in a .d.ts that your code depends on.
First, the compiler will try to locate a file that represents the imported module. To do so the compiler follows
one of two different strategies: Classic or Node. These strategies tell the compiler where to look for mod‐
uleA .
If that didn't work and if the module name is non-relative (and in the case of "moduleA" , it is), then the
compiler will attempt to locate an ambient module declaration. We'll cover non-relative imports next.
Finally, if the compiler could not resolve the module, it will log an error. In this case, the error would be
something like error TS2307: Cannot find module 'moduleA'.
A relative import is one that starts with / , ./ or ../ . Some examples include:
A relative import is resolved relative to the importing file and cannot resolve to an ambient module declara-
tion. You should use relative imports for your own modules that are guaranteed to maintain their relative lo-
cation at runtime.
A non-relative import can be resolved relative to baseUrl , or through path mapping, which we'll cover be-
low. They can also resolve to ambient module declarations. Use non-relative paths when importing any of
your external dependencies.
409
typescript
Note: node module resolution is the most-commonly used in the TypeScript community and is rec-
ommended for most projects. If you are having resolution problems with import s and export s in
TypeScript, try setting moduleResolution: "node" to see if it fixes the issue.
Classic
This used to be TypeScript's default resolution strategy. Nowadays, this strategy is mainly present for back-
ward compatibility.
A relative import will be resolved relative to the importing file. So import { b } from "./moduleB" in
source file /root/src/folder/A.ts would result in the following lookups:
1. /root/src/folder/moduleB.ts
2. /root/src/folder/moduleB.d.ts
For non-relative module imports, however, the compiler walks up the directory tree starting with the direc-
tory containing the importing file, trying to locate a matching definition file.
For example:
1. /root/src/folder/moduleB.ts
2. /root/src/folder/moduleB.d.ts
3. /root/src/moduleB.ts
4. /root/src/moduleB.d.ts
5. /root/moduleB.ts
6. /root/moduleB.d.ts
7. /moduleB.ts
8. /moduleB.d.ts
Node
This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime. The full
Node.js resolution algorithm is outlined in Node.js module documentation.
410
Module Resolution
To understand what steps the TS compiler will follow, it is important to shed some light on Node.js modules.
Traditionally, imports in Node.js are performed by calling a function named require . The behavior Node.js
takes will differ depending on if require is given a relative path or a non-relative path.
Relative paths are fairly straightforward. As an example, let's consider a file located at /root/src/module‐
A.js , which contains the import var x = require("./moduleB"); Node.js resolves that import in the fol-
lowing order:
2. Ask the folder /root/src/moduleB if it contains a file named package.json that specifies a "main"
module. In our example, if Node.js found the file /root/src/moduleB/package.json containing {
"main": "lib/mainModule.js" } , then Node.js will refer to /root/src/moduleB/lib/mainModule.js .
3. Ask the folder /root/src/moduleB if it contains a file named index.js . That file is implicitly consid-
ered that folder's "main" module.
You can read more about this in Node.js documentation on file modules and folder modules.
However, resolution for a non-relative module name is performed differently. Node will look for your mod-
ules in special folders named node_modules . A node_modules folder can be on the same level as the cur-
rent file, or higher up in the directory chain. Node will walk up the directory chain, looking through each
node_modules until it finds the module you tried to load.
Following up our example above, consider if /root/src/moduleA.js instead used a non-relative path and
had the import var x = require("moduleB"); . Node would then try to resolve moduleB to each of the
locations until one worked.
1. /root/src/node_modules/moduleB.js
2. /root/src/node_modules/moduleB/package.json (if it specifies a "main" property)
3. /root/src/node_modules/moduleB/index.js
4. /root/node_modules/moduleB.js
5. /root/node_modules/moduleB/package.json (if it specifies a "main" property)
6. /root/node_modules/moduleB/index.js
7. /node_modules/moduleB.js
8. /node_modules/moduleB/package.json (if it specifies a "main" property)
9. /node_modules/moduleB/index.js
You can read more about the process in Node.js documentation on loading modules from node_modules .
411
typescript
TypeScript will mimic the Node.js run-time resolution strategy in order to locate definition files for modules
at compile-time. To accomplish this, TypeScript overlays the TypeScript source file extensions ( .ts , .tsx ,
and .d.ts ) over Node's resolution logic. TypeScript will also use a field in package.json named types
to mirror the purpose of "main" - the compiler will use it to find the "main" definition file to consult.
1. /root/src/moduleB.ts
2. /root/src/moduleB.tsx
3. /root/src/moduleB.d.ts
4. /root/src/moduleB/package.json (if it specifies a types property)
5. /root/src/moduleB/index.ts
6. /root/src/moduleB/index.tsx
7. /root/src/moduleB/index.d.ts
Recall that Node.js looked for a file named moduleB.js , then an applicable package.json , and then for
an index.js .
Similarly, a non-relative import will follow the Node.js resolution logic, first looking up a file, then looking up
an applicable folder. So import { b } from "moduleB" in source file /root/src/moduleA.ts would re-
sult in the following lookups:
1. /root/src/node_modules/moduleB.ts
2. /root/src/node_modules/moduleB.tsx
3. /root/src/node_modules/moduleB.d.ts
4. /root/src/node_modules/moduleB/package.json (if it specifies a types property)
5. /root/src/node_modules/@types/moduleB.d.ts
6. /root/src/node_modules/moduleB/index.ts
7. /root/src/node_modules/moduleB/index.tsx
8. /root/src/node_modules/moduleB/index.d.ts
9. /root/node_modules/moduleB.ts
10. /root/node_modules/moduleB.tsx
11. /root/node_modules/moduleB.d.ts
12. /root/node_modules/moduleB/package.json (if it specifies a types property)
13. /root/node_modules/@types/moduleB.d.ts
14. /root/node_modules/moduleB/index.ts
15. /root/node_modules/moduleB/index.tsx
16. /root/node_modules/moduleB/index.d.ts
17. /node_modules/moduleB.ts
18. /node_modules/moduleB.tsx
412
Module Resolution
19. /node_modules/moduleB.d.ts
20. /node_modules/moduleB/package.json (if it specifies a types property)
21. /node_modules/@types/moduleB.d.ts
22. /node_modules/moduleB/index.ts
23. /node_modules/moduleB/index.tsx
24. /node_modules/moduleB/index.d.ts
Don't be intimidated by the number of steps here - TypeScript is still only jumping up directories twice at
steps (9) and (17). This is really no more complex than what Node.js itself is doing.
The TypeScript compiler has a set of additional flags to inform the compiler of transformations that are ex-
pected to happen to the sources to generate the final output.
It is important to note that the compiler will not perform any of these transformations; it just uses these
pieces of information to guide the process of resolving a module import to its definition file.
Base URL
Using a baseUrl is a common practice in applications using AMD module loaders where modules are "de-
ployed" to a single folder at run-time. The sources of these modules can live in different directories, but a
build script will put them all together.
Setting baseUrl informs the compiler where to find modules. All module imports with non-relative names
are assumed to be relative to the baseUrl .
value of baseUrl command line argument (if given path is relative, it is computed based on current
directory)
value of baseUrl property in 'tsconfig.json' (if given path is relative, it is computed based on the location
of 'tsconfig.json')
Note that relative module imports are not impacted by setting the baseUrl, as they are always resolved rel-
ative to their importing files.
You can find more documentation on baseUrl in RequireJS and SystemJS documentation.
413
typescript
Path mapping
Sometimes modules are not directly located under baseUrl. For instance, an import to a module "jquery"
would be translated at runtime to "node_modules/jquery/dist/jquery.slim.min.js" . Loaders use a
mapping configuration to map module names to files at run-time, see RequireJs documentation and
SystemJS documentation.
The TypeScript compiler supports the declaration of such mappings using paths property in tsconfig.j‐
son files. Here is an example for how to specify the paths property for jquery .
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to
"baseUrl"
}
}
}
Please notice that paths are resolved relative to baseUrl . When setting baseUrl to a value other than
"." , i.e. the directory of tsconfig.json , the mappings must be changed accordingly. Say, you set
"baseUrl": "./src" in the above example, then jquery should be mapped to
"../node_modules/jquery/dist/jquery" .
Using paths also allows for more sophisticated mappings including multiple fall back locations. Consider a
project configuration where only some modules are available in one location, and the rest are in another. A
build step would put them all together in one place. The project layout may look like:
projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": ["*", "generated/*"]
}
}
}
This tells the compiler for any module import that matches the pattern "*" (i.e. all values), to look in two
locations:
414
Module Resolution
1. "*" : meaning the same name unchanged, so map <moduleName> => <baseUrl>/<moduleName>
2. "generated/*" meaning the module name with an appended prefix "generated", so map
<moduleName> => <baseUrl>/generated/<moduleName>
Following this logic, the compiler will attempt to resolve the two imports as such:
import 'folder1/file2':
1. pattern '*' is matched and wildcard captures the whole module name
2. try first substitution in the list: '*' -> folder1/file2
3. result of substitution is non-relative name - combine it with baseUrl ->
projectRoot/folder1/file2.ts .
4. File exists. Done.
import 'folder2/file3':
1. pattern '*' is matched and wildcard captures the whole module name
2. try first substitution in the list: '*' -> folder2/file3
3. result of substitution is non-relative name - combine it with baseUrl ->
projectRoot/folder2/file3.ts .
4. File does not exist, move to the second substitution
5. second substitution 'generated/*' -> generated/folder2/file3
6. result of substitution is non-relative name - combine it with baseUrl -> projectRoot/generated/fold‐
er2/file3.ts .
7. File exists. Done.
Using rootDirs , you can inform the compiler of the roots making up this "virtual" directory; and thus the
compiler can resolve relative modules imports within these "virtual" directories as if they were merged to-
gether in one directory.
src
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports './view2')
415
typescript
Files in src/views are user code for some UI controls. Files in generated/templates are UI template
binding code auto-generated by a template generator as part of the build. A build step will copy the files in
/src/views and /generated/templates/views to the same directory in the output. At run-time, a view
can expect its template to exist next to it, and thus should import it using a relative name as
"./template" .
To specify this relationship to the compiler, use rootDirs . rootDirs specify a list of roots whose contents
are expected to merge at run-time. So following our example, the tsconfig.json file should look like:
{
"compilerOptions": {
"rootDirs": ["src/views", "generated/templates/views"]
}
}
Every time the compiler sees a relative module import in a subfolder of one of the rootDirs , it will at-
tempt to look for this import in each of the entries of rootDirs .
The flexibility of rootDirs is not limited to specifying a list of physical source directories that are logically
merged. The supplied array may include any number of ad hoc, arbitrary directory names, regardless of
whether they exist or not. This allows the compiler to capture sophisticated bundling and runtime features
such as conditional inclusion and project specific loader plugins in a type safe way.
Consider an internationalization scenario where a build tool automatically generates locale specific bundles
by interpolating a special path token, say #{locale} , as part of a relative module path such as ./#{lo‐
cale}/messages . In this hypothetical setup the tool enumerates supported locales, mapping the abstracted
path into ./zh/messages , ./de/messages , and so forth.
Assume that each of these modules exports an array of strings. For example ./zh/messages might
contain:
{
"compilerOptions": {
"rootDirs": ["src/zh", "src/de", "src/#{locale}"]
}
}
The compiler will now resolve import messages from './#{locale}/messages' to import messages
from './zh/messages' for tooling purposes, allowing development in a locale agnostic manner without
compromising design time support.
416
Module Resolution
Let's say we have a sample application that uses the typescript module. app.ts has an import like im‐
port * as ts from "typescript" .
│ tsconfig.json
├───node_modules
│ └───typescript
│ └───lib
│ typescript.d.ts
└───src
app.ts
tsc --traceResolution
417
typescript
Final result
Using --noResolve
Normally the compiler will attempt to resolve all module imports before it starts the compilation process.
Every time it successfully resolves an import to a file, the file is added to the set of files the compiler will
process later on.
The noResolve compiler options instructs the compiler not to "add" any files to the compilation that were
not passed on the command line. It will still try to resolve the module to files, but if the file is not specified,
it will not be included.
For instance:
app.ts
418
Module Resolution
Common Questions
Why does a module in the exclude list still get picked up by the
compiler?
tsconfig.json turns a folder into a “project”. Without specifying any “exclude” or “files” entries, all
files in the folder containing the tsconfig.json and all its sub-directories are included in your compilation.
If you want to exclude some of the files use “exclude” , if you would rather specify all the files instead of
letting the compiler look them up, use “files” .
That was tsconfig.json automatic inclusion. That does not embed module resolution as discussed above.
If the compiler identified a file as a target of a module import, it will be included in the compilation regard-
less if it was excluded in the previous steps.
So to exclude a file from the compilation, you need to exclude it and all files that have an import or ///
<reference path="..." /> directive to it.
Go to TOC
419
typescript
Starting with ECMAScript 2015, JavaScript has a concept of modules. TypeScript shares this concept.
Modules are executed within their own scope, not in the global scope; this means that variables, functions,
classes, etc. declared in a module are not visible outside the module unless they are explicitly exported us-
ing one of the export forms. Conversely, to consume a variable, function, class, interface, etc. exported
from a different module, it has to be imported using one of the import forms.
Modules are declarative; the relationships between modules are specified in terms of imports and exports at
the file level.
Modules import one another using a module loader. At runtime the module loader is responsible for locating
and executing all dependencies of a module before executing it. Well-known module loaders used in
JavaScript are Node.js's loader for CommonJS modules and the RequireJS loader for AMD modules in Web
applications.
In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered
a module. Conversely, a file without any top-level import or export declarations is treated as a script
whose contents are available in the global scope (and therefore to modules as well).
Export
Exporting a declaration
Any declaration (such as a variable, function, class, type alias, or interface) can be exported by adding the
export keyword.
StringValidator.ts
ZipCodeValidator.ts
Export statements
Export statements are handy when exports need to be renamed for consumers, so the above example can
be written as:
420
Modules
Re-exports
Often modules extend other modules, and partially expose some of their features. A re-export does not im-
port it locally, or introduce a local variable.
ParseIntBasedZipCodeValidator.ts
Optionally, a module can wrap one or more modules and combine all their exports using export * from
"module" syntax.
AllValidators.ts
Import
Importing is just about as easy as exporting from a module. Importing an exported declaration is done
through using one of the import forms below:
421
typescript