React Type Script
React Type Script
Tackling TypeScript
2020
Copyright © 2020 by Dr. Axel Rauschmayer
All rights reserved. This book or any portion thereof may not be reproduced or used in
any manner whatsoever without the express written permission of the publisher except
for the use of brief quotations in a book review or scholarly journal.
exploringjs.com
Contents
I Preliminaries 9
1 About this book 11
1.1 Where is the homepage of this book? . . . . . . . . . . . . . . . . . . . . 11
1.2 What is in this book? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3 What do I get for my money? . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4 How can I preview the content? . . . . . . . . . . . . . . . . . . . . . . . 12
1.5 How do I report errors? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.6 What do the notes with icons mean? . . . . . . . . . . . . . . . . . . . . . 12
1.7 Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2 Why TypeScript? 15
2.1 The benefits of using TypeScript . . . . . . . . . . . . . . . . . . . . . . . 15
2.2 The downsides of using TypeScript . . . . . . . . . . . . . . . . . . . . . 17
2.3 TypeScript myths . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3
4 CONTENTS
13 TypeScript enums: How do they work? What can they be used for? 77
13.1 The basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
13.2 Specifying enum member values (advanced) . . . . . . . . . . . . . . . . 80
13.3 Downsides of numeric enums . . . . . . . . . . . . . . . . . . . . . . . . 83
13.4 Use cases for enums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
13.5 Enums at runtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
13.6 const enums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
13.7 Enums at compile time . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
13.8 Acknowledgment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
VI Miscellaneous 215
25 An overview of computing with types 217
25.1 Types as metavalues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
25.2 Generic types: factories for types . . . . . . . . . . . . . . . . . . . . . . 218
25.3 Union types and intersection types . . . . . . . . . . . . . . . . . . . . . 219
25.4 Control flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
25.5 Various other operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
8 CONTENTS
Part I
Preliminaries
9
Chapter 1
Contents
1.1 Where is the homepage of this book? . . . . . . . . . . . . . . . . . 11
1.2 What is in this book? . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3 What do I get for my money? . . . . . . . . . . . . . . . . . . . . . . 11
1.4 How can I preview the content? . . . . . . . . . . . . . . . . . . . . . 12
1.5 How do I report errors? . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.6 What do the notes with icons mean? . . . . . . . . . . . . . . . . . . 12
1.7 Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
• Part 1 is a quick start for TypeScript that teaches you the essentials quickly.
• Part 2 digs deeper into the language and covers many important topics in detail.
This book is not a reference, it is meant to complement the official TypeScript handbook.
Required knowledge: You must know JavaScript. If you want to refresh your knowl-
edge: My book “JavaScript for impatient programmers” is free to read online.
11
12 1 About this book
– PDF file
– ZIP archive with ad-free HTML
– EPUB file
– MOBI file
• Any future content that is added to this edition. How much I can add depends on
the sales of this book.
Reading instructions
Explains how to best read the content (in which order, what to omit, etc.).
External content
Points to additional, external, content.
Git repository
Mentions a relevant Git repository.
Tip
Gives a tip.
Question
Asks and answers a question (think FAQ).
1.7 Acknowledgements 13
Warning
Warns about a pitfall, etc.
Details
Provides additional details, similar to a footnote.
1.7 Acknowledgements
People who contributed to this book are acknowledged in the chapters.
14 1 About this book
Chapter 2
Why TypeScript?
Contents
2.1 The benefits of using TypeScript . . . . . . . . . . . . . . . . . . . . 15
2.1.1 More errors are detected statically (without running code) . . . 15
2.1.2 Documenting parameters is good practice anyway . . . . . . . 16
2.1.3 TypeScript provides an additional layer of documentation . . 16
2.1.4 Type definitions for JavaScript improve auto-completion . . . 16
2.1.5 TypeScript makes refactorings safer . . . . . . . . . . . . . . . 17
2.1.6 TypeScript can compile new features to older code . . . . . . . 17
2.2 The downsides of using TypeScript . . . . . . . . . . . . . . . . . . 17
2.3 TypeScript myths . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3.1 TypeScript code is heavyweight . . . . . . . . . . . . . . . . . 17
2.3.2 TypeScript is an attempt to replace JavaScript with C# or Java 18
You can skip this chapter if you are already sure that you will learn and use TypeScript.
If you are still unsure – this chapter is my sales pitch.
15
16 2 Why TypeScript?
Another example:
const a = 0;
const b = true;
const result = a + b;
This time, the error message for the last line is:
/**
* @param {number} num - The number to convert to string
* @returns {string} `num`, converted to string
*/
function toString(num) {
return String(num);
}
Specifying the types via {number} and {string} is not required, but the descriptions in
English mention them, too.
If we use TypeScript’s notation to document types, we get the added benefit of this in-
formation being checked for consistency:
And I do indeed find it easier to understand TypeScript code bases than JavaScript code
bases: TypeScript provides an additional layer of documentation.
This additional documentation also helps when working in teams because it is clearer
how code is to be used and TypeScript often warns us if we are doing something wrong.
An alternative to using TypeScript’s syntax, is to provide all type information via JSDoc
comments – like we did at the beginning of this chapter. In that case, TypeScript can
2.2 The downsides of using TypeScript 17
also check code for consistency and generate type definitions. For more information, see
chapter “Type Checking JavaScript Files” in the TypeScript handbook.
The only locations where this TypeScript code is different from JavaScript code, are line
A and line B.
There are a variety of styles in which TypeScript is written:
• In an object-oriented programming (OOP) style with classes and OOP patterns
• In a functional programming (FP) style with functional patterns
• In a mix of OOP and FP
• And so on
Book on JavaScript:
• If you see a JavaScript feature in this book that you don’t understand, you can
look it up in my book “JavaScript for impatient programmers” which is free to
read online. Some of the “Further reading” sections at the ends of chapters refer
to this book.
Books on TypeScript:
• The “TypeScript Handbook” is a good reference for the language. I see “Tackling
TypeScript” as complementary to that book.
• “TypeScript Deep Dive” by Basarat Ali Syed
More material:
• The “TypeScript Language Specification” explains the lower levels of the language.
• Marius Schulz publishes blog posts on TypeScript and the email newsletter “Type-
Script Weekly”.
• The TypeScript repository has type definitions for the complete ECMAScript stan-
dard library. Reading them is an easy way of practicing TypeScript’s type notation.
19
20 3 Free resources on TypeScript
Part II
21
Chapter 4
Contents
4.1 The structure of TypeScript projects . . . . . . . . . . . . . . . . . . 23
4.1.1 tsconfig.json . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.2 Programming TypeScript via an integrated development environ-
ment (IDE) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3 Other files produced by the TypeScript compiler . . . . . . . . . . . 25
4.3.1 In order to use npm packages from TypeScript, we need type
information . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.4 Using the TypeScript compiler for plain JavaScript files . . . . . . . 26
This chapter gives the bird’s eye view of how TypeScript works: What is the structure of
a typical TypeScript project? What is compiled and how? How can we use IDEs to write
TypeScript?
Explanations:
23
24 4 How does TypeScript work? The bird’s eye view
4.1.1 tsconfig.json
The contents of tsconfig.json look as follows:
{
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist",
"module": "commonjs",
···
}
}
• Building (compiling TypeScript files to JavaScript files): Here, we have two choices.
– We can run a build tool via an external command line. For example, the Type-
Script compiler tsc has a --watch mode that watches input files and compiles
them to output files whenever they change. As a consequence, whenever we
save a TypeScript file in the IDE, we immediately get the corresponding out-
put file(s).
– We can run tsc from within Visual Studio Code. In order to do so, it must
be installed either inside project that we are currently working on or globally
(via the Node.js package manager npm).
With building, we get a complete list of errors. For more information on compiling
TypeScript from within Visual Studio Code, see the official documentation for that
IDE.
TypeScript is often not delivered via .ts files, but via .js files and .d.ts files:
• The JavaScript code contains the actual functionality and can be consumed via
plain JavaScript.
• The declaration files help programming editors with auto-completion and similar
services. This information enables plain JavaScript to be consumed via TypeScript.
However, we even profit from it if we work with plain JavaScript because it gives
us better auto-completion and more.
A source map specifies for each part of the output code in main.js, which part of the
input code in main.ts produced it. Among other things, this information enables run-
time environments to execute JavaScript code, while showing the line numbers of the
TypeScript code in error messages.
4.3.1 In order to use npm packages from TypeScript, we need type in-
formation
The npm registry is a huge repository of JavaScript code. If we want to use a JavaScript
package from TypeScript, we need type information for it:
• The package itself may include .d.ts files or even the complete TypeScript code.
• If it doesn’t, we may still be able to use it: DefinitelyTyped is a repository of decla-
ration files that people have written for plain JavaScript packages.
Contents
5.1 The TypeScript Playground . . . . . . . . . . . . . . . . . . . . . . . 27
5.2 TS Node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
The Playground is very useful for quick experiments and demos. It can save both Type-
Script code snippets and compiler settings into URLs, which is great for sharing such
snippets with others. This is an example of such a URL:
https://www.typescriptlang.org/play/#code/MYewdgzgLgBFDuBLYBTGBeGA
KAHgLhmgCdEwBzASgwD4YcYBqOgbgChXRIQAbFAOm4gyWBMhRYA5AEMARsAkUKzIA
5.2 TS Node
TS Node is a TypeScript version of Node.js. Its use cases are:
$ ts-node
> const twice = (x: string) => x + x;
> twice('abc')
'abcabc'
27
28 5 Trying out TypeScript
> twice(123)
Error TS2345: Argument of type '123' is not assignable
to parameter of type 'string'.
• TS Node enables some JavaScript tools to directly execute TypeScript code. It auto-
matically compiles TypeScript code to JavaScript code and passes it on to the tools,
without us having to do anything. The following shell command demonstrates
how that works with the JavaScript unit test framework Mocha:
mocha --require ts-node/register --ui qunit testfile.ts
Contents
6.1 Test assertions (dynamic) . . . . . . . . . . . . . . . . . . . . . . . . 29
6.2 Type assertions (static) . . . . . . . . . . . . . . . . . . . . . . . . . . 30
This chapter explains functionality that is used in the code examples, but not part of
TypeScript proper.
assert.deepEqual(
[...['a', 'b'], ...['c', 'd']],
['a', 'b', 'c', 'd']);
assert.throws(
() => eval('null.myProperty'),
TypeError);
29
30 6 Notation used in this book
The import statement in the first line makes use of strict assertion mode (which uses ===,
not ==). It is usually omitted in code examples.
Contents
7.1 What you’ll learn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
7.2 Specifying the comprehensiveness of type checking . . . . . . . . . 32
7.3 Types in TypeScript . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
7.4 Type annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
7.5 Type inference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
7.6 Specifying types via type expressions . . . . . . . . . . . . . . . . . 34
7.7 The two language levels: dynamic vs. static . . . . . . . . . . . . . . 35
7.8 Type aliases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7.9 Typing Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7.9.1 Arrays as lists . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7.9.2 Arrays as tuples . . . . . . . . . . . . . . . . . . . . . . . . . . 36
7.10 Function types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
7.10.1 A more complicated example . . . . . . . . . . . . . . . . . . 37
7.10.2 Return types of function declarations . . . . . . . . . . . . . . 37
7.10.3 Optional parameters . . . . . . . . . . . . . . . . . . . . . . . 38
7.10.4 Rest parameters . . . . . . . . . . . . . . . . . . . . . . . . . . 38
7.11 Union types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
7.11.1 By default, undefined and null are not included in types . . . 39
7.11.2 Making omissions explicit . . . . . . . . . . . . . . . . . . . . 39
7.12 Optional vs. default value vs. undefined|T . . . . . . . . . . . . . . 40
7.13 Typing objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
7.13.1 Typing objects-as-records via interfaces . . . . . . . . . . . . . 41
7.13.2 TypeScript’s structural typing vs. nominal typing . . . . . . . 41
7.13.3 Object literal types . . . . . . . . . . . . . . . . . . . . . . . . 42
7.13.4 Optional properties . . . . . . . . . . . . . . . . . . . . . . . . 42
7.13.5 Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
7.14 Type variables and generic types . . . . . . . . . . . . . . . . . . . . 43
7.14.1 Example: a container for values . . . . . . . . . . . . . . . . . 44
31
32 7 The essentials of TypeScript
interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U
): U;
// ···
}
You may think that this is cryptic. And I agree with you! But (as I hope to prove) this
syntax is relatively easy to learn. And once you understand it, it gives you immediate,
precise and comprehensive summaries of how code behaves – without having to read
long descriptions in English.
• --noImplicitAny: If TypeScript can’t infer a type, we must specify it. This mainly
applies to parameters of functions and methods: With this settings, we must an-
notate them.
• --noImplicitThis: Complain if the type of this isn’t clear.
• --alwaysStrict: Use JavaScript’s strict mode whenever possible.
7.3 Types in TypeScript 33
• --strictNullChecks: null is not part of any type (other than its own type, null)
and must be explicitly mentioned if it is a acceptable value.
• --strictFunctionTypes: enables stronger checks for function types.
• --strictPropertyInitialization: Properties in class definitions must be initial-
ized, unless they can have the value undefined.
We will see more compiler options later in this book, when we get to creating npm pack-
ages and web apps with TypeScript. The TypeScript handbook has comprehensive doc-
umentation on them.
TypeScript brings an additional layer to JavaScript: static types. These only exist when
compiling or type-checking source code. Each storage location (variable, property, etc.)
has a static type that predicts its dynamic values. Type checking ensures that these pre-
dictions come true.
And there is a lot that can be checked statically (without running the code). If, for ex-
ample the parameter num of a function toString(num) has the static type number, then
the function call toString('abc') is illegal, because the argument 'abc' has the wrong
static type.
Both number and string are type expressions that specify the types of storage locations.
34 7 The essentials of TypeScript
Type inference is not guesswork: It follows clear rules (similar to arithmetic) for deriving
types where they haven’t been specified explicitly. In this case, the return statement
applies a function String() that maps arbitrary values to strings, to a value num of type
number and returns the result. That’s why the inferred return type is string.
If the type of a location is neither explicitly specified nor inferrable, TypeScript uses the
type any for it. This is the type of all values and a wildcard, in that we can do everything
if a value has that type.
With --strict, any is only allowed if we use it explicitly. In other words: Every location
must have an explicit or inferred static type. In the following example, parameter num
has neither and we get a compile-time error:
There are many ways of combining basic types to produce new, compound types. For
example, via type operators that combine types similarly to how the set operators union
(∪) and intersection (∩) combine sets. We’ll see how to do that soon.
7.7 The two language levels: dynamic vs. static 35
• The dynamic level is managed by JavaScript and consists of code and values, at
runtime.
• The static level is managed by TypeScript (excluding JavaScript) and consists of
static types, at compile time.
• At the dynamic level, we use JavaScript to declare a variable undef and initialize
it with the value undefined.
• At the static level, we use TypeScript to specify that variable undef has the static
type undefined.
Note that the same syntax, undefined, means different things depending on whether it
is used at the dynamic level or at the static level.
• List: All elements have the same type. The length of the Array varies.
• Tuple: The length of the Array is fixed. The elements generally don’t have the
same type.
Normally, TypeScript can infer the type of a variable if there is an assignment. In this
case, we actually have to help it, because with an empty Array, it can’t determine the
type of the elements.
The type annotation is needed for Arrays-as-tuples because, for Array literals, TypeScript
infers list types, not tuple types:
// %inferred-type: number[]
let point = [7, 5];
Another example for tuples is the result of Object.entries(obj): an Array with one
[key, value] pair for each property of obj.
assert.deepEqual(
entries,
[[ 'a', 1 ], [ 'b', 2 ]]);
This type comprises every function that accepts a single parameter of type number and
return a string. Let’s use this type in a type annotation:
Normally, we must specify parameter types for functions. But in this case, the type of
num in line B can be inferred from the function type in line A and we can omit it:
If we omit the type annotation for toString, TypeScript infers a type from the arrow
function:
We are using a function type to describe the parameter callback of stringify123(). Due
to this type annotation, TypeScript rejects the following function call.
// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
// Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);
void is a special return type for a function: It tells TypeScript that the function always
returns undefined.
It may do so explicitly:
function f1(): void {
return undefined;
}
Or it may do so implicitly:
function f2(): void {}
However, such a function cannot explicitly return values other than undefined:
function f3(): void {
// @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
return 'abc';
}
38 7 The essentials of TypeScript
TypeScript only lets us make the function call in line A if we make sure that callback
isn’t undefined (which it is if the parameter was omitted).
assert.deepEqual(
createPoint(),
[0, 0]);
assert.deepEqual(
createPoint(1, 2),
[1, 2]);
Default values make parameters optional. We can usually omit type annotations, be-
cause TypeScript can infer the types. For example, it can infer that x and y both have the
type number.
assert.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3);
stringOrNumber has the type string|number. The result of the type expression s|t is the
set-theoretic union of the types s and t (interpreted as sets).
Conversely, in TypeScript, undefined and null are handled by separate, disjoint types.
We need union types such as undefined|string and null|string, if we want to allow
them:
Note that TypeScript does not force us to initialize immediately (as long as we don’t read
from the variable before initializing it):
function stringify123(
callback: null | ((num: number) => string)) {
const num = 123;
if (callback === null) { // (A)
callback = String;
}
return callback(num); // (B)
}
assert.equal(
stringify123(null),
'123');
Once again, we have to handle the case of callback not being a function (line A) before
we can make the function call in line B. If we hadn’t done so, TypeScript would have
reported an error in that line.
If the parameter is optional, it can be omitted. In that case, it has the value undefined:
assert.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omit
If the parameter has a default value, that value is used when the parameter is either
omitted or set to undefined:
assert.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omit
If the parameter has a union type, it can’t be omitted, but we can set it to undefined:
assert.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK
• Records: A fixed number of properties that are known at development time. Each
property can have a different type.
We are ignoring objects-as-dictionaries in this chapter – they are covered in §16.4.5 “Index
signatures: objects as dicts”. As an aside, Maps are usually a better choice for dictionaries,
anyway.
interface Point {
x: number;
y: number;
}
interface Point {
x: number,
y: number,
}
interface Point {
x: number;
42 7 The essentials of TypeScript
y: number;
}
function pointToString(pt: Point) {
return `(${pt.x}, ${pt.y})`;
}
assert.equal(
pointToString({x: 5, y: 7}), // compatible structure
'(5, 7)');
Conversely, in Java’s nominal type system, we must explicitly declare with each class
which interfaces it implements. Therefore, a class can only implement interfaces that
exist at its creation time.
type Point = {
x: number;
y: number;
};
One benefit of object literal types is that they can be used inline:
interface Person {
name: string;
company?: string;
}
In the following example, both john and jane match the interface Person:
7.13.5 Methods
Interfaces can also contain methods:
7.14 Type variables and generic types 43
interface Point {
x: number;
y: number;
distance(other: Point): number;
}
interface HasMethodDef {
simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
simpleMethod: (flag: boolean) => void;
}
Similarly:
• Normal functions exist at the dynamic level, are factories for values and have pa-
rameters representing values. Parameters are declared between parentheses:
• Generic types exist at the static level, are factories for types and have parameters
representing types. Parameters are declared between angle brackets:
44 7 The essentials of TypeScript
Value is a type variable. One or more type variables can be introduced between angle
brackets.
class SimpleStack<Elem> {
#data: Array<Elem> = [];
push(x: Elem): void {
this.#data.push(x);
}
pop(): Elem {
const result = this.#data.pop();
if (result === undefined) {
throw new Error();
}
return result;
}
get length() {
return this.#data.length;
}
}
Class SimpleStack has the type parameter T. Single uppercase letters such as T are often
used for type parameters.
When we instantiate the class, we also provide a value for the type parameter:
stringStack.push('second');
assert.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second');
Thanks to type inference (based on the argument of new Map()), we can omit the type
parameters:
// %inferred-type: number
const num1 = identity<number>(123);
Due to type inference, we can once again omit the type parameter:
// %inferred-type: 123
const num2 = identity(123);
Note that TypeScript inferred the type 123, which is a set with one number and more
specific than the type number.
const obj = {
identity<Arg>(arg: Arg): Arg {
return arg;
46 7 The essentials of TypeScript
},
};
The return type of fillArray() is inferred, but we also could have specified it explicitly.
We can omit the type parameter when calling fillArray() (line A) because TypeScript
can infer T from the parameter elem:
// %inferred-type: string[]
const arr1 = fillArray<string>(3, '*');
assert.deepEqual(
arr1, ['*', '*', '*']);
// %inferred-type: string[]
const arr2 = fillArray(3, '*'); // (A)
interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U
): U;
// ···
}
• method .concat() has zero or more parameters (defined via a rest parameter).
Each of those parameters has the type T[]|T. That is, it is either an Array of T
values or a single T value.
• method .reduce() introduces its own type variable U. U is used to express the fact
that the following entities all have the same type:
– Result of callback()
– Optional parameter firstState of .reduce()
– Result of .reduce()
In addition to state, callback() has the following parameters:
– element, which has the same type T as the Array elements
– index; a number
– array with elements of type T
48 7 The essentials of TypeScript
Chapter 8
Contents
8.1 Required knowledge . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.2 Limitations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
8.3 The repository ts-demo-npm-cjs . . . . . . . . . . . . . . . . . . . . 50
8.4 .gitignore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8.5 .npmignore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8.6 package.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8.6.1 Scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
8.6.2 dependencies vs. devDependencies . . . . . . . . . . . . . . . 52
8.6.3 More information on package.json . . . . . . . . . . . . . . . 53
8.7 tsconfig.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
8.8 TypeScript code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
8.8.1 index.ts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
8.8.2 index_test.ts . . . . . . . . . . . . . . . . . . . . . . . . . . 54
This chapter describes how to use TypeScript to create packages for the package manager
npm that are based on the CommonJS module format.
49
50 8 Creating CommonJS-based npm packages via TypeScript
• CommonJS modules – a module format that originated in, and was designed for,
server-side JavaScript. It was popularized by the server-side JavaScript platform
Node.js. CommonJS modules preceded JavaScript’s built-in ECMAScript modules
and are still much used and very well supported by tooling (IDEs, built tools, etc.).
• TypeScript’s modules – whose syntax is based on ECMAScript modules. However,
they are often compiled to CommonJS modules.
• npm packages – directories with files that are installed via the npm package man-
ager. They can contain CommonJS modules, ECMAScript modules, and various
other files.
8.2 Limitations
In this chapter, we are using what TypeScript currently supports best:
• All our TypeScript code is compiled to CommonJS modules with the filename ex-
tension .js.
• All external imports are CommonJS modules, too.
Especially on Node.js, TypeScript currently doesn’t really support ECMAScript modules
and filename extensions other than .js.
Apart from the package.json for the package, the repository contains:
• ts/src/index.ts: the actual code of the package
• ts/test/index_test.ts: a test for index.ts
• tsconfig.json: configuration data for the TypeScript compiler
package.json contains scripts for compiling:
8.4 .gitignore
This file lists the directories that we don’t want to check into git:
node_modules/
dist/
Explanations:
8.5 .npmignore
When it comes to which files should and should not be uploaded to the npm registry, we
have different needs than we did for git. Therefore, in addition to .gitignore, we also
need the file .npmignore:
ts/
8.6 package.json
package.json looks like this:
{
···
"type": "commonjs",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"scripts": {
"clean": "shx rm -rf dist/*",
"build": "tsc",
"watch": "tsc --watch",
"test": "mocha --ui qunit",
"testall": "mocha --ui qunit dist/test",
"prepack": "npm run clean && npm run build"
},
"// devDependencies": {
"@types/node": "Needed for unit test assertions (assert.equal() etc.)",
52 8 Creating CommonJS-based npm packages via TypeScript
• type: The value "commonjs" means that .js files are interpreted as CommonJS
modules.
• main: If there is a so-called bare import that only mentions the name of the current
package, then this is the module that will be imported.
• types points to a declaration file with all the type definitions for the current pack-
age.
8.6.1 Scripts
Property scripts defines various commands that can be invoked via npm run. For exam-
ple, the script clean is invoked via npm run clean. The previous package.json contains
the following scripts:
• clean uses the cross-platform package shx to delete the compilation results via
its implementation of the Unix shell command rm. shx supports a variety of shell
commands with the benefit of not needing a separate package for each command
we may want to use.
• build and watch use the TypeScript compiler tsc to compile the TypeScript files
according to tsconfig.json. tsc must be installed globally or locally (inside the
current package), usually via the npm package typescript.
• test and testall use the unit test framework Mocha to run one test or all tests.
• prepack: This script is run run before a tarball is packed (due to npm pack, npm
publish, or an installation from git).
Note that when we are using an IDE, we don’t need the scripts build and watch because
we can let the IDE build the artifacts. But they are needed for the script prepack.
Packages whose names start with @types/ provide TypeScript type definitions for pack-
ages that don’t have any. Without the former, we can’t use the latter. Are these normal
dependencies or dev dependencies? It depends:
• If the type definitions of our package refer to type definitions in another package,
that package is a normal dependency.
• Otherwise, the package is only needed during development time and a dev depen-
dency.
8.7 tsconfig.json
{
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist",
"target": "es2019",
"lib": [
"es2019"
],
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"declaration": true,
"sourceMap": true
}
}
• target: What is the targeted ECMAScript version? If the TypeScript code uses a
feature that is not supported by the targeted version, then it is compiled to equiv-
alent code that only uses supported features.
• lib: What platform features should TypeScript be aware of? Possibilities include
the ECMAScript standard library and the DOM of browsers. The Node.js API is
supported differently, via the package @types/node.
The remaining options are explained by the official documentation for tsconfig.json.
54 8 Creating CommonJS-based npm packages via TypeScript
It uses function endsWith() of the library Lodash. That’s why Lodash is a normal depen-
dency – it is needed at runtime.
8.8.2 index_test.ts
This file contains a unit test for index.ts:
import { strict as assert } from 'assert';
import { removeSuffix } from '../src/index';
test('removeSuffix()', () => {
assert.equal(
removeSuffix('myfile.txt', '.txt'),
'myfile');
assert.throws(() => removeSuffix('myfile.txt', 'abc'));
});
As you can see, we are running the compiled version of the test (in directory dist/), not
the TypeScript code.
For more information on the unit test framework Mocha, see its homepage.
Chapter 9
Contents
9.1 Required knowledge . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
9.2 Limitations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
9.3 The repository ts-demo-webpack . . . . . . . . . . . . . . . . . . . . 56
9.4 package.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
9.5 webpack.config.js . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
9.6 tsconfig.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
9.7 index.html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
9.8 main.ts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
9.9 Installing, building and running the web app . . . . . . . . . . . . . 60
9.9.1 Building in Visual Studio Code . . . . . . . . . . . . . . . . . 61
9.10 Using webpack without a loader: webpack-no-loader.config.js . . 61
This chapter describes how to create web apps via TypeScript and webpack. We will only
be using the DOM API, not a particular frontend framework.
55
56 9 Creating web apps via TypeScript and webpack
9.2 Limitations
In this chapter, we stick with what is best supported by TypeScript: CommonJS modules,
bundled as script files.
ts-demo-webpack/
build/ (created on demand)
html/
index.html
package.json
ts/
src/
main.ts
tsconfig.json
webpack.config.js
• Input:
– The TypeScript files in ts/
– All JavaScript code that is installed via npm and imported by the TypeScript
files
– The HTML files in html/
• Output – directory build/ with the complete web app:
– The TypeScript files are compiled to JavaScript code, combined with the npm-
installed JavaScript and written to the script file build/main-bundle.js.
This process is called bundling and main-bundle.js is a bundle file.
– Each HTML file is copied to build/.
• Copying the files in html/ to build/ is done via the webpack plugin copy-webpack-
plugin.
– Either webpack directly compiles TypeScript files into the bundle, with the
help of the loader ts-loader.
– Or we compile the TypeScript files ourselves, to Javascript files in the direc-
tory dist/ (like we did in the previous chpater). Then webpack doesn’t need
a loader and only bundles JavaScript files.
Most of this chapter is about using webpack with ts-loader. At the end, we briefly
look at the other workflow.
9.4 package.json 57
9.4 package.json
{
"private": true,
"scripts": {
"tsc": "tsc",
"tscw": "tsc --watch",
"wp": "webpack",
"wpw": "webpack --watch",
"serve": "http-server build"
},
"dependencies": {
"@types/lodash": "···",
"copy-webpack-plugin": "···",
"http-server": "···",
"lodash": "···",
"ts-loader": "···",
"typescript": "···",
"webpack": "···",
"webpack-cli": "···"
}
}
• "private": true means that npm doesn’t complain if we don’t provide a package
name and a package version.
• Scripts:
– tsc, tscw: These scripts invoke the TypeScript compiler directly. We don’t
need them if we use webpack with ts-loader. However, they are useful if we
use webpack without ts-loader (as demonstrated at the end of this chapter).
– wp: runs webpack once, compile everything.
– wpw: runs webpack in watch mode, where it watches the input files and only
compiles files that change.
– serve: runs the server http-server and serves the directory build/ with the
fully assembled web app.
• Dependencies:
– Four packages related to webpack:
* webpack: the core of webpack
* webpack-cli: a command line interface for the core
* ts-loader: a loader for .ts files that compiles them to JavaScript
* copy-webpack-plugin: a plugin that copies files from one location to an-
other one
– Needed by ts-loader: typescript
– Serves the web app: http-server
– Library plus type definitions that the TypeScript code uses: lodash,
@types/lodash
58 9 Creating web apps via TypeScript and webpack
9.5 webpack.config.js
This is how we configure webpack:
module.exports = {
···
entry: {
main: "./ts/src/main.ts",
},
output: {
path: path.resolve(__dirname, 'build'),
filename: "[name]-bundle.js",
},
resolve: {
// Add ".ts" and ".tsx" as resolvable extensions.
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
// all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
{ test: /\.tsx?$/, loader: "ts-loader" },
],
},
plugins: [
new CopyWebpackPlugin([
{
from: './html',
}
]),
],
};
Properties:
• entry: An entry point is the file where webpack starts collecting the data for an
output bundle. First it adds the entry point file to the bundle, then the imports of
the entry point, then the imports of the imports, etc. The value of property entry is
an object whose property keys specify names of entry points and whose property
values specify paths of entry points.
• output specifies the path of the output bundle. [name] is mainly useful when there
are multiple entry points (and therefore multiple output bundles). It is replaced
with the name of the entry point when assembling the path.
• plugins configures plugins which can change and augment webpack’s behavior in
a variety of ways.
9.6 tsconfig.json
{
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist",
"target": "es2019",
"lib": [
"es2019",
"dom"
],
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"sourceMap": true
}
}
The option outDir is not needed if we use webpack with ts-loader. However, we’ll
need it if we use webpack without a loader (as explained later in this chapter).
9.7 index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>ts-demo-webpack</title>
</head>
<body>
<div id="output"></div>
<script src="main-bundle.js"></script>
</body>
</html>
The <div> with the ID "output" is where the web app displays its output. main-
bundle.js contains the bundled code.
60 9 Creating web apps via TypeScript and webpack
9.8 main.ts
• Step 1: We use Lodash’s function template() to turn a string with custom template
syntax into a function compiled() that maps data to HTML. The string defines two
blanks to be filled in via data:
– <%- heading %>
– <%- dateTimeString %>
• Step 2: Apply compiled() to the data (an object with two properties) to generate
HTML.
npm install
Then we need to run webpack (which was installed during the previous step) via a script
in package.json:
From now on, webpack watches the files in the repository for changes and rebuilds the
web app whenever it detects any.
In a different command line, we can now start a web server that serves the contents of
build/ on localhost:
If we go to the URL printed out by the web server, we can see the web app in action.
Note that simple reloading may not be enough to see the results after changes – due to
caching. You may have to force-reload by pressing shift when reloading.
9.10 Using webpack without a loader: webpack-no-loader.config.js 61
We can now start webpack via “Run Build Task…” from the “Terminal” menu.
module.exports = {
entry: {
main: "./dist/src/main.js",
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name]-bundle.js',
},
plugins: [
new CopyWebpackPlugin([
{
from: './html',
}
]),
],
};
Why would we want to produce intermediate files before bundling them? One benefit
is that we can use Node.js to run unit tests for some of the TypeScript code.
62 9 Creating web apps via TypeScript and webpack
Chapter 10
Contents
10.1 Three strategies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
10.2 Strategy: mixed JavaScript/TypeScript code bases . . . . . . . . . . 64
10.3 Strategy: adding type information to plain JavaScript files . . . . . 64
10.4 Strategy: migrating large projects by snapshot testing the TypeScript
errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
10.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
This chapter gives an overview of strategies for migrating code bases from JavaScript to
TypeScript. It also mentions material for further reading.
• We can support a mix of JavaScript and TypeScript files for our code base. We start
with only JavaScript files and then switch more and more files to TypeScript.
• We can keep our current (non-TypeScript) build process and our JavaScript-only
code base. We add static type information via JSDoc comments and use TypeScript
as a type checker (not as a compiler). Once everything is correctly typed, we switch
to TypeScript for building.
• For large projects, there may be too many TypeScript errors during migration.
Then snapshot tests can help us find fixed errors and new errors.
More information:
63
64 10 Strategies for migrating to TypeScript
At first, there are only JavaScript files. Then, one by one, we switch files to TypeScript.
While we do so, our code base keeps being compiled.
{
"compilerOptions": {
···
"allowJs": true
}
}
More information:
This is how we specify static types for plain JavaScript via JSDoc comments:
/**
* @param {number} x - The first operand
* @param {number} y - The second operand
* @returns {number} The sum of both operands
*/
function add(x, y) {
10.4 Strategy: migrating large projects by snapshot testing the TypeScript errors 65
return x + y;
}
More information:
• §4.4 “Using the TypeScript compiler for plain JavaScript files”
• “How we gradually migrated to TypeScript at Unsplash” by Oliver Joseph Ash
10.5 Conclusion
We have taken a quick look at strategies for migrating to TypeScript. Two more tips:
• Start your migration with experiments: Play with your code base and try out var-
ious strategies before committing to one of them.
• Then lay out a clear plan for going forward. Talk to your team w.r.t. prioritization:
– Sometimes finishing the migration quickly may take priority.
– Sometimes the code remaining fully functional during the migration may be
more important.
– And so on…
66 10 Strategies for migrating to TypeScript
Part III
Basic types
67
Chapter 11
Contents
11.1 Three questions for each perspective . . . . . . . . . . . . . . . . . . 69
11.2 Perspective 1: types are sets of values . . . . . . . . . . . . . . . . . 70
11.3 Perspective 2: type compatibility relationships . . . . . . . . . . . . 70
11.4 Nominal type systems vs. structural type systems . . . . . . . . . . . 71
11.5 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
What are types in TypeScript? This chapter describes two perspectives that help with
understanding them.
69
70 11 What is a type in TypeScript? Two perspectives
1. If myVariable has the type MyType, that means that all values that can be assigned
to myVariable must be elements of the set MyType.
3. The union type of the types Type1, Type2, and Type3 is the set-theoretic union of
the sets that define them.
• The source code has locations and each location has a static type. In a TypeScript-
aware editor, we can see the static type of a location if we hover above it with the
cursor.
1. The static type of a variable determines what can be assigned to it. That always
depends on static types. For example, in a function call toString(123), the static
type of 123 must be assignable to the static type of the parameter of toString().
2. myVariable has type MyType if the static type of myVariable is assignable to MyType.
4. How union types work is defined via the type relationship apparent members.
An interesting trait of TypeScript’s type system is that the same variable can have differ-
ent static types at different locations:
// %inferred-type: any[]
const arr = [];
arr.push(123);
11.4 Nominal type systems vs. structural type systems 71
// %inferred-type: number[]
arr;
arr.push('abc');
// %inferred-type: (string | number)[]
arr;
• The static type Src of an actual parameter (e.g., provided via a function call)
• The static type Trg of the corresponding formal parameter (e.g., specified as part
of a function definition)
This often means checking if Src is a subtype of Trg. Two approaches for this check are
(roughly):
• In a nominal or nominative type system, two static types are equal if they have the
same identity (“name”). One type is a subtype of another if their subtype relation-
ship was defined explicitly.
– Languages with nominal typing are C++, Java, C#, Swift, and Rust.
• In a structural type system, two static types are equal if they have the same structure
(if their parts have the same names and the same types). One type Sub is a subtype
of another type Sup if Sub has all parts of Sup (and possibly others) and each part
of Sub has a subtype of the corresponding part of Sup.
– Languages with structural typing are OCaml/ReasonML and TypeScript.
The following code produces a type error in line A with nominal type systems, but is
legal with TypeScript’s structural type system because class A and class B have the same
structure:
class A {
name = 'A';
}
class B {
name = 'B';
}
const someVariable: A = new B(); // (A)
interface Point {
x: number;
y: number;
}
const point: Point = {x: 1, y: 2}; // OK
72 11 What is a type in TypeScript? Two perspectives
Contents
12.1 TypeScript’s two top types . . . . . . . . . . . . . . . . . . . . . . . . 73
12.2 The top type any . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
12.2.1 Example: JSON.parse() . . . . . . . . . . . . . . . . . . . . . 74
12.2.2 Example: String() . . . . . . . . . . . . . . . . . . . . . . . . 74
12.3 The top type unknown . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
In TypeScript, any and unknown are types that contain all values. In this chapter, we
examine what they are and what they can be used for.
The top type […] is the universal type, sometimes called the universal supertype
as all other types in any given type system are subtypes […]. In most cases
it is the type which contains every possible [value] in the type system of
interest.
That is, when viewing types as sets of values (for more information on what types are,
see §11 “What is a type in TypeScript? Two perspectives”), any and unknown are sets that
contain all values. As an aside, TypeScript also has the bottom type never, which is the
empty set.
73
74 12 The top types any and unknown
// Normally only allowed for Arrays and types with index signatures
value[123];
}
storageLocation = null;
storageLocation = true;
storageLocation = {};
With any we lose any protection that is normally given to us by TypeScript’s static type
system. Therefore, it should only be used as a last resort, if we can’t use more specific
types or unknown.
JSON.parse() was added to TypeScript before the type unknown existed. Otherwise, its
return type would probably be unknown.
Before we can perform any operation on values of type unknown, we must first narrow
their types via:
• Type assertions:
// Type assertion:
(value as number).toFixed(2); // OK
}
• Equality:
value * 5; // OK
}
}
• Type guards:
value.length; // OK
}
}
• Assertion functions:
assertIsRegExp(value);
// %inferred-type: RegExp
76 12 The top types any and unknown
value;
value.test('abc'); // OK
}
Contents
13.1 The basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
13.1.1 Numeric enums . . . . . . . . . . . . . . . . . . . . . . . . . . 78
13.1.2 String-based enums . . . . . . . . . . . . . . . . . . . . . . . . 79
13.1.3 Heterogeneous enums . . . . . . . . . . . . . . . . . . . . . . 79
13.1.4 Omitting initializers . . . . . . . . . . . . . . . . . . . . . . . 79
13.1.5 Casing of enum member names . . . . . . . . . . . . . . . . . 80
13.1.6 Quoting enum member names . . . . . . . . . . . . . . . . . . 80
13.2 Specifying enum member values (advanced) . . . . . . . . . . . . . 80
13.2.1 Literal enum members . . . . . . . . . . . . . . . . . . . . . . 81
13.2.2 Constant enum members . . . . . . . . . . . . . . . . . . . . . 81
13.2.3 Computed enum members . . . . . . . . . . . . . . . . . . . . 82
13.3 Downsides of numeric enums . . . . . . . . . . . . . . . . . . . . . 83
13.3.1 Downside: logging . . . . . . . . . . . . . . . . . . . . . . . . 83
13.3.2 Downside: loose type-checking . . . . . . . . . . . . . . . . . 83
13.3.3 Recommendation: prefer string-based enums . . . . . . . . . . 83
13.4 Use cases for enums . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
13.4.1 Use case: bit patterns . . . . . . . . . . . . . . . . . . . . . . . 84
13.4.2 Use case: multiple constants . . . . . . . . . . . . . . . . . . . 85
13.4.3 Use case: more self-descriptive than booleans . . . . . . . . . 86
13.4.4 Use case: better string constants . . . . . . . . . . . . . . . . . 87
13.5 Enums at runtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
13.5.1 Reverse mappings . . . . . . . . . . . . . . . . . . . . . . . . 88
13.5.2 String-based enums at runtime . . . . . . . . . . . . . . . . . 89
13.6 const enums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
13.6.1 Compiling non-const enums . . . . . . . . . . . . . . . . . . . 89
13.6.2 Compiling const enums . . . . . . . . . . . . . . . . . . . . . 90
77
78 13 TypeScript enums: How do they work? What can they be used for?
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);
Explanations:
• The entries No and Yes are called the members of the enum NoYes.
• Each enum member has a name and a value. For example, the first member has the
name No and the value 0.
• The part of a member definition that starts with an equals sign and specifies a value
is called an initializer.
• As in object literals, trailing commas are allowed and ignored.
We can use members as if they were literals such as true, 123, or 'abc' – for example:
function toGerman(value: NoYes) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
}
}
assert.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja');
13.1 The basics 79
enum NoYes {
No = 'No',
Yes = 'Yes',
}
assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');
enum Enum {
One = 'One',
Two = 'Two',
Three = 3,
Four = 4,
}
assert.deepEqual(
[Enum.One, Enum.Two, Enum.Three, Enum.Four],
['One', 'Two', 3, 4]
);
Heterogeneous enums are not used often because they have few applications.
Alas, TypeScript only supports numbers and strings as enum member values. Other
values, such as symbols, are not allowed.
• We can omit the initializer of the first member. Then that member has the value 0
(zero).
• We can omit the initializer of a member if the previous member has a number value.
Then the current member has that value plus one.
enum NoYes {
No,
Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);
enum Enum {
A,
B,
C = 'C',
D = 'D',
E = 8, // (A)
F,
}
assert.deepEqual(
[Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
[0, 1, 'C', 'D', 8, 9]
);
Note that we can’t omit the initializer in line A because the value of the preceding mem-
ber is not a number.
enum HttpRequestField {
'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}
assert.equal(HttpRequestField['Accept-Charset'], 1);
There is no way to compute the names of enum members. Object literals support com-
puted property keys via square brackets.
• A constant enum member is initialized via an expression whose result can be com-
puted at compile time.
In the previous list, members that are mentioned earlier are less flexible but support more
features. Read on for more information.
• either implicitly
• or via a number literal (incl. negated number literals)
• or via a string literal.
If an enum has only literal members, we can use those members as types (similar to how,
e.g., number literals can be used as types):
enum NoYes {
No = 'No',
Yes = 'Yes',
}
function func(x: NoYes.No) { // (A)
return x;
}
func(NoYes.No); // OK
Additionally, literal enums support exhaustiveness checks (which we’ll look at later).
This is an example of an enum whose members are all constant (we’ll see later how that
enum is used):
enum Perm {
UserRead = 1 << 8, // bit 8
UserWrite = 1 << 7,
UserExecute = 1 << 6,
GroupRead = 1 << 5,
GroupWrite = 1 << 4,
GroupExecute = 1 << 3,
AllRead = 1 << 2,
AllWrite = 1 << 1,
AllExecute = 1 << 0,
}
enum NoYesNum {
No = 123,
Yes = Math.random(), // OK
}
This was a numeric enum. String-based enums and heterogeneous enums are more lim-
ited. For example, we cannot use method invocations to specify member values:
enum NoYesStr {
No = 'No',
// @ts-expect-error: Computed values are not permitted in
// an enum with string valued members.
Yes = ['Y', 'e', 's'].join(''),
}
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 0
// 1
I think if we did TypeScript over again and still had enums, we’d have made
a separate construct for bit flags.
How enums are used for bit patterns is demonstrated soon in more detail.
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 'No'
// 'Yes'
Not even strings that are equal to values of members are allowed (line A).
Node.js doesn’t do this, but we could use an enum to work with these flags:
enum Perm {
UserRead = 1 << 8, // bit 8
UserWrite = 1 << 7,
UserExecute = 1 << 6,
GroupRead = 1 << 5,
GroupWrite = 1 << 4,
GroupExecute = 1 << 3,
AllRead = 1 << 2,
AllWrite = 1 << 1,
AllExecute = 1 << 0,
}
13.4 Use cases for enums 85
The main idea behind bit patterns is that there is a set of flags and that any subset of those
flags can be chosen.
Therefore, using real sets to choose subsets is a more straightforward way of performing
the same task:
enum Perm {
UserRead = 'UserRead',
UserWrite = 'UserWrite',
UserExecute = 'UserExecute',
GroupRead = 'GroupRead',
GroupWrite = 'GroupWrite',
GroupExecute = 'GroupExecute',
AllRead = 'AllRead',
AllWrite = 'AllWrite',
AllExecute = 'AllExecute',
}
function writeFileSync(
thePath: string, permissions: Set<Perm>, content: string) {
// ···
}
writeFileSync(
'/tmp/hello.txt',
new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
'Hello!');
enum LogLevel {
off = 'off',
info = 'info',
warn = 'warn',
error = 'error',
}
One benefit of the enum is that the constant names are grouped and nested inside the
namespace LogLevel.
Another one is that we automatically get the type LogLevel for them. If we want such a
type for the constants, we need more work:
type LogLevel =
| typeof off
| typeof info
| typeof warn
| typeof error
;
For more information on this approach, see §14.1.3 “Unions of symbol singleton types”.
For example, to represent whether a list is ordered or not, we can use a boolean:
class List1 {
isOrdered: boolean;
// ···
}
However, an enum is more self-descriptive and has the additional benefit that we can
add more alternatives later if we need to.
enum ErrorHandling {
throwOnError = 'throwOnError',
showErrorsInContent = 'showErrorsInContent',
}
function convertToHtml2(markdown: string, errorHandling: ErrorHandling) {
// ···
}
assert.deepEqual(
createRegExp('abc', GLOBAL),
/abc/ug);
assert.deepEqual(
createRegExp('abc', 'g'), // OK
/abc/ug);
enum Globalness {
Global = 'g',
notGlobal = '',
}
assert.deepEqual(
88 13 TypeScript enums: How do they work? What can they be used for?
createRegExp('abc', Globalness.Global),
/abc/ug);
assert.deepEqual(
// @ts-expect-error: Argument of type '"g"' is not assignable to parameter of type 'Glo
createRegExp('abc', 'g'), // error
/abc/ug);
NoYes[0] = "No";
NoYes[1] = "Yes";
// Dynamic lookup:
assert.equal(NoYes['Yes'], 1);
Numeric enums also support a reverse mapping from member values to member names:
assert.equal(NoYes[1], 'Yes');
One use case for reverse mappings is printing the name of an enum member:
enum NoYes {
No = 'NO!',
Yes = 'YES!',
}
var NoYes;
(function (NoYes) {
NoYes["No"] = "NO!";
NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));
enum NoYes {
No = 'No',
Yes = 'Yes',
90 13 TypeScript enums: How do they work? What can they be used for?
function toGerman(value) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
}
}
Now the representation of the enum as a construct disappears and only the values of its
members remain:
function toGerman(value) {
switch (value) {
13.7 Enums at compile time 91
}
}
assert.throws(
// @ts-expect-error: Argument of type '"Maybe"' is not assignable to
// parameter of type 'NoYes'.
() => toGerman1('Maybe'),
/^TypeError: Unsupported value: "Maybe"$/);
We can take one more measure. The following code performs an exhaustiveness check:
TypeScript will warn us if we forget to consider all enum members.
class UnsupportedValueError extends Error {
constructor(value: never) {
super('Unsupported value: ' + value);
}
}
How does the exhaustiveness check work? For every case, TypeScript infers the type of
value:
default:
// %inferred-type: never
value;
throw new UnsupportedValueError(value);
}
}
In the default case, TypeScript infers the type never for value because we never get
there. If however, we add a member .Maybe to NoYes, then the inferred type of value is
NoYes.Maybe. And that type is statically incompatible with the type never of the param-
eter of new UnsupportedValueError(). That’s why we get the following error message
at compile time:
If we add a member to NoYes, then TypeScript complains that toGerman4() may return
undefined.
13.8 Acknowledgment
• Thanks to Disqus user @spira_mirabilis for their feedback to this chapter.
Chapter 14
Alternatives to enums in
TypeScript
Contents
14.1 Unions of singleton values . . . . . . . . . . . . . . . . . . . . . . . 95
14.1.1 Primitive literal types . . . . . . . . . . . . . . . . . . . . . . . 96
14.1.2 Unions of string literal types . . . . . . . . . . . . . . . . . . . 96
14.1.3 Unions of symbol singleton types . . . . . . . . . . . . . . . . 98
14.1.4 Conclusion of this section: union types vs. enums . . . . . . . 100
14.2 Discriminated unions . . . . . . . . . . . . . . . . . . . . . . . . . . 101
14.2.1 Step 1: the syntax tree as a class hierarchy . . . . . . . . . . . 101
14.2.2 Step 2: the syntax tree as a union type of classes . . . . . . . . 102
14.2.3 Step 3: the syntax tree as a discriminated union . . . . . . . . 102
14.2.4 Discriminated unions vs. normal union types . . . . . . . . . . 105
14.3 Object literals as enums . . . . . . . . . . . . . . . . . . . . . . . . . 106
14.3.1 Object literals with string-valued properties . . . . . . . . . . 108
14.3.2 Upsides and downsides of using object literals as enums . . . 108
14.4 Enum pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
14.5 Summary of enums and enum alternatives . . . . . . . . . . . . . . 109
14.6 Acknowledgement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
The previous chapter explored how TypeScript enums work. In this chapter, we take a
look at alternatives to enums.
95
96 14 Alternatives to enums in TypeScript
A singleton type is a type with one element. Primitive literal types are singleton types:
It is important to be aware of the two language levels at play here (we have already en-
countered those levels earlier in this book). Consider the following variable declaration:
• Overloading on string parameters which enables the first argument of the follow-
ing method call to determine the type of the second argument:
elem.addEventListener('click', myEventHandler);
• We can use a union of primitive literal types to define a type by enumerating its
members:
enum NoYesEnum {
No = 'No',
Yes = 'Yes',
}
function toGerman1(value: NoYesEnum): string {
switch (value) {
case NoYesEnum.No:
return 'Nein';
case NoYesEnum.Yes:
return 'Ja';
}
}
14.1 Unions of singleton values 97
assert.equal(toGerman1(NoYesEnum.No), 'Nein');
assert.equal(toGerman1(NoYesEnum.Yes), 'Ja');
The type NoYesStrings is the union of the string literal types 'No' and 'Yes'. The union
type operator | is related to the set-theoretic union operator ∪.
The following code demonstrates that exhaustiveness checks work for unions of string
literal types:
// @ts-expect-error: Function lacks ending return statement and
// return type does not include 'undefined'. (2366)
function toGerman3(value: NoYesStrings): string {
switch (value) {
case 'Yes':
return 'Ja';
}
}
We forgot the case for 'No' and TypeScript warns us that the function may return values
that are not strings.
We could have also checked exhaustiveness more explicitly:
class UnsupportedValueError extends Error {
constructor(value: never) {
super('Unsupported value: ' + value);
}
}
Now TypeScript warns us that we reach the default case if value is 'No'.
One downside of string literal unions is that non-member values can mistaken for mem-
bers:
This is logical because the Spanish 'no' and the English 'no' are the same value. The
actual problem is that there is no way to give them different identities.
Instead of unions of string literal types, we can also use unions of symbol singleton types.
Let’s start with a different enum this time:
enum LogLevel {
off = 'off',
info = 'info',
warn = 'warn',
error = 'error',
}
| typeof info
| typeof warn
| typeof error
;
Why do we need typeof here? off etc. are values and can’t appear in type equations.
The type operator typeof fixes this issue by converting values to types.
Can we inline the symbols (instead of referring to separate const declarations)? Alas,
the operand of the type operator typeof must be an identifier or a “path” of identifiers
separated by dots. Therefore, this syntax is illegal:
Can we use let instead of const to declare the variables? (That’s not necessarily an
improvement but still an interesting question.)
We can’t because we need the narrower types that TypeScript infers for const-declared
variables:
// %inferred-type: symbol
let letSymbol1 = Symbol('letSymbol1');
With let, LogLevel would simply have been an alias for symbol.
const assertions normally solve this kind of problem. But they don’t work in this case:
return 'error';
}
}
assert.equal(
getName(warn), 'warn');
14.1.3.5 Unions of symbol singleton types vs. unions of string literal types
Recall this example where the Spanish 'no' was confused with the English 'no':
But they also differ. Downsides of unions of symbol singleton types are:
• It’s slightly harder to migrate from them to different constructs (should it be nec-
essary): It’s easier to find where enum member values are mentioned.
• They are not a custom TypeScript language construct and therefore closer to plain
JavaScript.
• String enums are only type-safe at compile time. Unions of symbol singleton types
are additionally type-safe at runtime.
– This matters especially if our compiled TypeScript code interacts with plain
JavaScript code.
To understand how they work, consider the data structure syntax tree that represents
expressions such as:
1 + 2 + 3
• A number
• The addition of two syntax trees
Next steps:
Note: Trailing commas in argument lists are allowed in JavaScript since ECMAScript
2016.
class NumberValue2 {
constructor(public numberValue: number) {}
}
class Addition2 {
constructor(public operand1: SyntaxTree2, public operand2: SyntaxTree2) {}
}
type SyntaxTree2 = NumberValue2 | Addition2; // (A)
Since NumberValue2 and Addition2 don’t have a superclass, they don’t need to invoke
super() in their constructors.
interface NumberValue3 {
kind: 'number-value';
numberValue: number;
}
interface Addition3 {
kind: 'addition';
14.2 Discriminated unions 103
operand1: SyntaxTree3;
operand2: SyntaxTree3;
}
type SyntaxTree3 = NumberValue3 | Addition3;
We have switched from classes to interfaces and therefore from instances of classes to
plain objects.
The interfaces of a discriminated union must have at least one property in common and
that property must have a different value for each one of them. That property is called
the discriminant or tag. The discriminant of SyntaxTree3 is .kind. Its types are string
literal types.
Compare:
We don’t need the type annotation in line A, but it helps ensure that the data has the
correct structure. If we don’t do it here, we’ll find out about problems later.
In the next example, the type of tree is a discriminated union. Every time we check its
discriminant (line C), TypeScript updates its static type accordingly:
In line A, we haven’t checked the discriminant .kind, yet. Therefore, the current type of
tree is still SyntaxTree3 and we can’t access property .numberValue in line B (because
only one of the types of the union has this property).
In line D, TypeScript knows that .kind is 'number-value' and can therefore infer the
type NumberValue3 for tree. That’s why accessing .numberValue in the next line is OK,
this time.
We conclude this step with an example of how to implement functions for discriminated
unions.
If there is an operation that can be applied to members of all subtypes, the approaches
for classes and discriminated unions differ:
• Object-oriented approach: With classes, it is common to use a polymorphic
method where each class has a different implementation.
• Functional approach: With discriminated unions, it is common to use a single func-
tion that handles all possibles cases and decides what to do by examining the dis-
criminant of its parameter.
The following example demonstrates the functional approach. The discriminant is ex-
amined in line A and determines which of the two switch cases is executed.
function syntaxTreeToString(tree: SyntaxTree3): string {
switch (tree.kind) { // (A)
case 'addition':
return syntaxTreeToString(tree.operand1)
+ ' + ' + syntaxTreeToString(tree.operand2);
case 'number-value':
return String(tree.numberValue);
}
}
• With the functional approach, we have to modify each function if we want to add
a new type. In contrast, adding new operations is simple.
The next two subsections explore two advantages of discriminated unions over normal
unions:
106 14 Alternatives to enums in TypeScript
With discriminated unions, values get descriptive property names. Let’s compare:
Normal union:
type FileGenerator = (webPath: string) => string;
type FileSource1 = string|FileGenerator;
Discriminated union:
interface FileSourceFile {
type: 'FileSourceFile',
nativePath: string,
}
interface FileSourceGenerator {
type: 'FileSourceGenerator',
fileGenerator: FileGenerator,
}
type FileSource2 = FileSourceFile | FileSourceGenerator;
Now people who read the source code immediately know what the string is: a native
pathname.
14.2.4.2 Benefit: We can also use it when the parts are indistinguishable
// %inferred-type: symbol
type TColor2 = // (B)
| typeof Color.red
| typeof Color.green
| typeof Color.blue
;
Alas, the type of each property of Color is symbol (line A) and TColor (line B) is an alias
for symbol. As a consequence, we can pass any symbol to toGerman() and TypeScript
won’t complain at compile time:
assert.equal(
toGerman(Color.green), 'grün');
assert.throws(
() => toGerman(Symbol())); // no static error!
A const assertion often helps in this kind of situation but not this time:
const ConstColor = {
red: Symbol('red'),
green: Symbol('green'),
blue: Symbol('blue'),
} as const;
// %inferred-type: symbol
ConstColor.red;
// %inferred-type: "red"
Color.red;
We need as const in line A so that the properties of Color don’t have the more general
type string. Then TColor also has a type that is more specific than string.
• Better at development time because we get exhaustiveness checks and can derive
a narrow type for the values (without using external constants).
• Worse at runtime because strings can be mistaken for enum values.
Downsides:
assert.equal(toGerman(Color.blue), 'blau');
Alas, TypeScript doesn’t perform exhaustiveness checks, which is why we get an error
in line A.
14.6 Acknowledgement
• Thanks to Kirill Sukhomlin for his suggestion on how to define TColor for an object
literal.
Chapter 15
Contents
15.1 Adding special values in band . . . . . . . . . . . . . . . . . . . . . 111
15.1.1 Adding null or undefined to a type . . . . . . . . . . . . . . . 112
15.1.2 Adding a symbol to a type . . . . . . . . . . . . . . . . . . . . 113
15.2 Adding special values out of band . . . . . . . . . . . . . . . . . . . 113
15.2.1 Discriminated unions . . . . . . . . . . . . . . . . . . . . . . . 113
15.2.2 Other kinds of union types . . . . . . . . . . . . . . . . . . . . 115
One way of understanding types is as sets of values. Sometimes there are two levels of
values:
In this chapter, we examine how we can add special values to base-level types.
interface InputStream {
getNextLine(): string;
}
At the moment, .getNextLine() only handles text lines, but not ends of files (EOFs).
How could we add support for EOF?
Possibilities include:
111
112 15 Adding special values to types
The next two subsections describe two ways in which we can introduce sentinel values.
interface InputStream {
getNextLine(): StreamValue;
}
Now, whenever we are using the value returned by .getNextLine(), TypeScript forces
us to consider both possibilities: strings and null – for example:
In line A, we can’t use the string method .startsWith() because line might be null.
We can fix this as follows:
Now, when execution reaches line A, we can be sure that line is not null.
15.2 Adding special values out of band 113
Why do we need typeof and can’t use EOF directly? That’s because EOF is a value, not
a type. The type operator typeof converts EOF to a type. For more information on the
different language levels of values and types, see §7.7 “The two language levels: dynamic
vs. static”.
Whatever value we pick for EOF, there is a risk of someone creating an Input-
Stream<typeof EOF> and adding that value to the stream.
The solution is to keep normal values and special values separate, so that they can’t be
mixed up. Special values existing separately is called out of band (think different channel).
interface InputStream<T> {
getNextValue(): InputStreamValue<T>;
}
// %inferred-type: NormalValue<T>
value; // (B)
Initially, the type of value is InputStreamValue<T> (line A). Then we exclude the value
'eof' for the discriminant .type and its type is narrowed to NormalValue<T> (line B).
That’s why we can access property .data in line C.
When deciding how to implement iterators, TC39 didn’t want to use a fixed sentinel
value. Otherwise, that value could appear in iterables and break code. One solution
would have been to pick a sentinel value when starting an iteration. TC39 instead opted
for a discriminated union with the common property .done:
interface IteratorYieldResult<TYield> {
done?: false; // boolean literal type
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true; // boolean literal type
value: TReturn;
}
Another possibility is to distinguish the member types via typeof and/or instance
checks:
type Union = [string] | number;
117
Chapter 16
Typing objects
Contents
16.1 Roles played by objects . . . . . . . . . . . . . . . . . . . . . . . . . 120
16.2 Types for objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
16.3 Object vs. object in TypeScript . . . . . . . . . . . . . . . . . . . . . 120
16.3.1 Plain JavaScript: objects vs. instances of Object . . . . . . . . 120
16.3.2 Object (uppercase “O”) in TypeScript: instances of class Object 121
16.3.3 object (lowercase “o”) in TypeScript: non-primitive values . . 122
16.3.4 Object vs. object: primitive values . . . . . . . . . . . . . . . 122
16.3.5 Object vs. object: incompatible property types . . . . . . . . 122
16.4 Object type literals and interfaces . . . . . . . . . . . . . . . . . . . 123
16.4.1 Differences between object type literals and interfaces . . . . . 123
16.4.2 Interfaces work structurally in TypeScript . . . . . . . . . . . . 125
16.4.3 Members of interfaces and object type literals . . . . . . . . . 125
16.4.4 Method signatures . . . . . . . . . . . . . . . . . . . . . . . . 126
16.4.5 Index signatures: objects as dicts . . . . . . . . . . . . . . . . . 127
16.4.6 Interfaces describe instances of Object . . . . . . . . . . . . . 128
16.4.7 Excess property checks: When are extra properties allowed? . 129
16.5 Type inference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
16.6 Other features of interfaces . . . . . . . . . . . . . . . . . . . . . . . 133
16.6.1 Optional properties . . . . . . . . . . . . . . . . . . . . . . . . 133
16.6.2 Read-only properties . . . . . . . . . . . . . . . . . . . . . . . 134
16.7 JavaScript’s prototype chains and TypeScript’s types . . . . . . . . . 135
16.8 Sources of this chapter . . . . . . . . . . . . . . . . . . . . . . . . . . 135
In this chapter, we will explore how objects and properties are typed statically in Type-
Script.
119
120 16 Typing objects
• Records have a fixed amount of properties that are known at development time.
Each property can have a different type.
• Dictionaries have an arbitrary number of properties whose names are not known
at development time. All property keys (strings and/or symbols) have the same
type, as have property values.
First and foremost, we will explore objects as records. We will briefly encounter objects
as dictionaries later in this chapter.
• Object with an uppercase “O” is the type of all instances of class Object:
// Interface
interface ObjectType {
prop: boolean;
}
let obj4: ObjectType;
In the next sections, we’ll examine all these ways of typing objects in more detail.
That means:
16.3 Object vs. object in TypeScript 121
On the other hand, we can also create objects that don’t have Object.prototype in their
prototype chains. For example, the following object does not have any prototype at all:
> const obj2 = Object.create(null);
> Object.getPrototypeOf(obj2)
null
interface ObjectConstructor {
/** Invocation via `new` */
new(value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
122 16 Typing objects
// ···
}
declare var Object: ObjectConstructor; // (C)
Observations:
• We have both a variable whose name is Object (line C) and a type whose name is
Object (line A).
• Direct instances of Object have no own properties, therefore Object.prototype
also matches Object (line B).
In TypeScript, object is the type of all non-primitive values (primitive values are un-
defined, null, booleans, numbers, bigints, strings). With this type, we can’t access any
properties of a value.
Why is that? Primitive values have all the properties required by Object because they
inherit Object.prototype:
With type Object, TypeScript complains if an object has a property whose type conflicts
with the corresponding property in interface Object:
With type object, TypeScript does not complain (because object does not specify any
properties and there can’t be any conflicts):
// Interface
interface ObjType2 {
a: boolean,
b: number;
c: string,
}
We can use either semicolons or commas as separators. Trailing separators are allowed
and optional.
16.4.1.1 Inlining
// Referenced interface:
function f2(x: ObjectInterface) {}
interface ObjectInterface {
prop: number;
}
interface PersonInterface {
first: string;
}
interface PersonInterface {
last: string;
}
const jane: PersonInterface = {
first: 'Jane',
last: 'Doe',
};
For Mapped types (line A), we need to use object type literals:
interface Point {
x: number;
y: number;
}
type PointCopy1 = {
[Key in keyof Point]: Point[Key]; // (A)
};
// Syntax error:
// interface PointCopy2 {
// [Key in keyof Point]: Point[Key];
// };
interface AddsStrings {
add(str: string): this;
};
this.result += str;
return this;
}
}
From now on, “interface” means “interface or object type literal” (unless
stated otherwise).
interface Point {
x: number;
y: number;
}
const point: Point = {x: 1, y: 2}; // OK
For more information on this topic, see §11.4 “Nominal type systems vs. structural type
systems”.
interface ExampleInterface {
// Property signature
myProperty: boolean;
// Method signature
myMethod(str: string): number;
// Index signature
[key: string]: any;
// Call signature
(num: number): string;
// Construct signature
new(str: string): ExampleInstance;
}
interface ExampleInstance {}
126 16 Typing objects
myProperty: boolean;
Note: The names of parameters (in this case: str) help with documenting how
things work but have no other purpose.
• Index signatures are needed to describe Arrays or objects that are used as dictio-
naries.
interface HasMethodDef {
simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
simpleMethod: (flag: boolean) => void;
}
We use an index signature (line A) to express that TranslationDict is for objects that
map string keys to string values:
interface TranslationDict {
[key:string]: string; // (A)
}
const dict = {
'yes': 'sí',
'no': 'no',
'maybe': 'tal vez',
};
assert.equal(
translate(dict, 'maybe'),
'tal vez');
Just like in plain JavaScript, TypeScript’s number property keys are a subset of the string
property keys (see “JavaScript for impatient programmers”). Accordingly, if we have
both a string index signature and a number index signature, the property type of the
former must be a supertype of the latter. The following example works because Object
is a supertype of RegExp:
interface StringAndNumberKeys {
[key: string]: Object;
128 16 Typing objects
If there are both an index signature and property and/or method signatures in an inter-
face, then the type of the index property value must also be a supertype of the type of
the property value and/or method.
interface I1 {
[key: string]: boolean;
interface I2 {
[key: string]: number;
myProp: number;
}
interface I3 {
[key: string]: () => string;
myMethod(): string;
}
In the following example, the parameter x of type {} is compatible with the return type
Object:
There are two ways (among others) in which this interface could be interpreted:
• Closed interpretation: It could describe all objects that have exactly the properties
.x and .y with the specified types. On other words: Those objects must not have
excess properties (more than the required properties).
• Open interpretation: It could describe all objects that have at least the properties
.x and .y. In other words: Excess properties are allowed.
TypeScript uses both interpretations. To explore how that works, we will use the follow-
ing function:
function computeDistance(point: Point) { /*...*/ }
However, if we use object literals directly, then excess properties are forbidden:
// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
// Object literal may only specify known properties, and 'z' does not
// exist in type 'Point'. (2345)
computeDistance({ x: 1, y: 2, z: 3 }); // error
computeDistance({x: 1, y: 2}); // OK
Why the stricter rules for object literals? They provide protection against typos in prop-
erty keys. We will use the following interface to demonstrate what that means.
interface Person {
first: string;
middle?: string;
last: string;
}
function computeFullName(person: Person) { /*...*/ }
130 16 Typing objects
Property .middle is optional and can be omitted (optional properties are covered later
in this chapter). To TypeScript, mistyping its name looks like omitting it and providing
an excess property. However, it still catches the typo because excess properties are not
allowed in this case:
16.4.7.2 Why are excess properties allowed if an object comes from somewhere else?
The idea is that if an object comes from somewhere else, we can assume that it has already
been vetted and will not have any typos. Then we can afford to be less careful.
If typos are not an issue, our goal should be maximizing flexibility. Consider the follow-
ing function:
interface HasYear {
year: number;
}
Without allowing excess properties for most values that are passed to getAge(), the use-
fulness of this function would be quite limited.
If an interface is empty (or the object type literal {} is used), excess properties are always
allowed:
interface Empty { }
interface OneProp {
myProp: number;
}
If we want to enforce that an object has no properties, we can use the following trick
(credit: Geoff Goodman):
interface WithoutProperties {
[key: string]: never;
}
interface Point {
x: number;
y: number;
}
const obj = { x: 1, y: 2, z: 3 };
computeDistance1(obj);
computeDistance1({ x: 1, y: 2, z: 3 } as Point); // OK
computeDistance3({ x: 1, y: 2, z: 3 }); // OK
132 16 Typing objects
We’ll continue with two examples where TypeScript not allowing excess properties, is
an issue.
Alas, even with a type assertion, there is still one type error:
function createIncrementor2(start = 0): Incrementor {
return {
counter: start,
inc() {
// @ts-expect-error: Property 'counter' does not exist on type
// 'Incrementor'. (2339)
this.counter++;
},
} as Incrementor;
}
The following comparison function can be used to sort objects that have the property
.dateStr:
function compareDateStrings(
a: {dateStr: string}, b: {dateStr: string}) {
if (a.dateStr < b.dateStr) {
return +1;
} else if (a.dateStr > b.dateStr) {
return -1;
} else {
return 0;
}
}
For example in unit tests, we may want to invoke this function directly with object literals.
TypeScript doesn’t let us do this and we need to use one of the workarounds.
// %inferred-type: Object
const obj1 = new Object();
// %inferred-type: any
const obj2 = Object.create(null);
// %inferred-type: {}
const obj3 = {};
// %inferred-type: object
const obj5 = Reflect.getPrototypeOf({});
In principle, the return type of Object.create() could be object. However, any allows
us to add and change properties of the result.
interface Name {
first: string;
middle?: string;
last: string;
}
An optional property can do everything that undefined|string can. We can even use
the value undefined for the former:
const obj1: Interf = { prop1: undefined, prop2: undefined };
Types such as undefined|string and null|string are useful if we want to make omis-
sions explicit. When people see such an explicitly omitted property, they know that it
exists but was switched off.
console.log(obj.prop); // OK
// property. (2540)
obj.prop = 2;
The downside of this approach is that some phenomena in JavaScript can’t be described
via TypeScript’s type system. The upside is that the type system is simpler.
Contents
17.1 Cheat sheet: classes in plain JavaScript . . . . . . . . . . . . . . . . 137
17.1.1 Basic members of classes . . . . . . . . . . . . . . . . . . . . . 138
17.1.2 Modifier: static . . . . . . . . . . . . . . . . . . . . . . . . . 138
17.1.3 Modifier-like name prefix: # (private) . . . . . . . . . . . . . . 138
17.1.4 Modifiers for accessors: get (getter) and set (setter) . . . . . . 139
17.1.5 Modifier for methods: * (generator) . . . . . . . . . . . . . . . 139
17.1.6 Modifier for methods: async . . . . . . . . . . . . . . . . . . . 140
17.1.7 Computed class member names . . . . . . . . . . . . . . . . . 140
17.1.8 Combinations of modifiers . . . . . . . . . . . . . . . . . . . . 140
17.1.9 Under the hood . . . . . . . . . . . . . . . . . . . . . . . . . . 141
17.1.10 More information on class definitions in plain JavaScript . . . 143
17.2 Non-public data slots in TypeScript . . . . . . . . . . . . . . . . . . 143
17.2.1 Private properties . . . . . . . . . . . . . . . . . . . . . . . . . 143
17.2.2 Private fields . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
17.2.3 Private properties vs. private fields . . . . . . . . . . . . . . . 145
17.2.4 Protected properties . . . . . . . . . . . . . . . . . . . . . . . 145
17.3 Private constructors . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
17.4 Initializing instance properties . . . . . . . . . . . . . . . . . . . . . 147
17.4.1 Strict property initialization . . . . . . . . . . . . . . . . . . . 147
17.4.2 Making constructor parameters public, private, or protected 149
17.5 Abstract classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
137
138 17 Class definitions in TypeScript
publicInstanceField = 1;
constructor() {
super();
}
publicPrototypeMethod() {
return 2;
}
}
static staticPublicField = 1;
static staticPublicMethod() {
return 2;
}
}
assert.equal(MyClass2.staticPublicField, 1);
assert.equal(MyClass2.staticPublicMethod(), 2);
#privateMethod() {
return 2;
}
17.1 Cheat sheet: classes in plain JavaScript 139
static accessPrivateMembers() {
// Private members can only be accessed from inside class definitions
const inst3 = new MyClass3();
assert.equal(inst3.#privateField, 1);
assert.equal(inst3.#privateMethod(), 2);
}
}
MyClass3.accessPrivateMembers();
TypeScript has been supporting private fields since version 3.8 but does not currently
support private methods.
class MyClass5 {
#name = 'Rumpelstiltskin';
assert.deepEqual(
[...inst6.publicPrototypeGeneratorMethod()],
['hello', 'world']);
class MyClass8 {
[publicInstanceFieldKey] = 1;
[publicPrototypeMethodKey]() {
return 2;
}
}
Comments:
• The main use case for this feature is symbols such as Symbol.iterator. But any
expression can be used inside the square brackets.
• We can compute the names of fields, methods, and accessors.
• We cannot compute the names of private members (which are always fixed).
Level Visibility
(instance)
(instance) #
17.1 Cheat sheet: classes in plain JavaScript 141
Level Visibility
static
static #
Methods (no level means that a construct exists at the prototype level):
Limitations of methods:
class ClassA {
static staticMthdA() {}
142 17 Class definitions in TypeScript
constructor(instPropA) {
this.instPropA = instPropA;
}
prototypeMthdA() {}
}
class ClassB extends ClassA {
static staticMthdB() {}
constructor(instPropA, instPropB) {
super(instPropA);
this.instPropB = instPropB;
}
prototypeMthdB() {}
}
const instB = new ClassB(0, 1);
Fig. 17.1 shows what the prototype chains look like that are created by ClassA and
ClassB.
… …
ClassA ClassA.prototype
__proto__ __proto__
prototype protoMthdA ƒ
staticMthdA ƒ constructor
ClassB ClassB.prototype
__proto__ __proto__
prototype protoMthdB ƒ
staticMthdB ƒ constructor
instB
__proto__
instPropA 0
instPropB 1
Figure 17.1: The classes ClassA and ClassB create two prototype chains: One for classes
(left-hand side) and one for instances (right-hand side).
17.2 Non-public data slots in TypeScript 143
• Private properties
• Private fields
class PersonPrivateProperty {
private name: string; // (A)
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}
We now get compile-time errors if we access that property in the wrong scope (line A):
assert.equal(
john.sayHello(), 'Hello John!');
However, private doesn’t change anything at runtime. There, property .name is indis-
tinguishable from a public property:
assert.deepEqual(
Object.keys(john),
['name']);
144 17 Class definitions in TypeScript
We can also see that private properties aren’t protected at runtime when we look at the
JavaScript code that the class is compiled to:
class PersonPrivateProperty {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}
class PersonPrivateField {
#name: string;
constructor(name: string) {
this.#name = name;
}
sayHello() {
return `Hello ${this.#name}!`;
}
}
This version of Person is mostly used the same way as the private property version:
assert.equal(
john.sayHello(), 'Hello John!');
However, this time, the data is completely encapsulated. Using the private field syntax
outside classes is even a JavaScript syntax error. That’s why we have to use eval() in
line A so that we can execute this code:
assert.throws(
() => eval('john.#name'), // (A)
{
name: 'SyntaxError',
message: "Private field '#name' must be declared in "
+ "an enclosing class",
});
assert.deepEqual(
Object.keys(john),
[]);
// Omitted: __classPrivateFieldGet
This code uses a common technique for keeping instance data private:
class PrivatePerson {
private name: string;
constructor(name: string) {
this.name = name;
}
146 17 Class definitions in TypeScript
sayHello() {
return `Hello ${this.name}!`;
}
}
class PrivateEmployee extends PrivatePerson {
private company: string;
constructor(name: string, company: string) {
super(name);
this.company = company;
}
sayHello() {
// @ts-expect-error: Property 'name' is private and only
// accessible within class 'PrivatePerson'. (2341)
return `Hello ${this.name} from ${this.company}!`; // (A)
}
}
We can fix the previous example by switching from private to protected in line A (we
also switch in line B, for consistency’s sake):
class ProtectedPerson {
protected name: string; // (A)
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}
class ProtectedEmployee extends ProtectedPerson {
protected company: string; // (B)
constructor(name: string, company: string) {
super(name);
this.company = company;
}
sayHello() {
return `Hello ${this.name} from ${this.company}!`; // OK
}
}
class DataContainer {
#data: string;
static async create() {
const data = await Promise.resolve('downloaded'); // (A)
return new this(data);
}
private constructor(data: string) {
this.#data = data;
}
getData() {
return 'DATA: '+this.#data;
}
}
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
In real-world code, we would use fetch() or a similar Promise-based API to load data
asynchronously in line A.
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class Point {
x = 0;
y = 0;
148 17 Class definitions in TypeScript
// No constructor needed
}
class Point {
x!: number; // (A)
y!: number; // (B)
constructor() {
this.initProperties();
}
initProperties() {
this.x = 0;
this.y = 0;
}
}
In the following example, we also need definite assignment assertions. Here, we set up
instance properties via the constructor parameter props:
Notes:
class Point2 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
If we use private or protected instead of public, then the corresponding instance prop-
erties are private or protected (not public).
class StringBuilder {
string = '';
add(str: string) {
this.string += str;
}
}
abstract class Printable {
toString() {
const out = new StringBuilder();
150 17 Class definitions in TypeScript
this.print(out);
return out.string;
}
abstract print(out: StringBuilder): void;
}
On the other hand, there are the concrete subclasses Entries and Entry:
• An abstract class can be seen as an interface where some members already have
implementations.
17.5 Abstract classes 151
• While a class can implement multiple interfaces, it can only extend at most one
abstract class.
• “Abstractness” only exists at compile time. At runtime, abstract classes are normal
classes and abstract methods don’t exist (due to them only providing compile-time
information).
• Abstract classes can be seen as templates where each abstract method is a blank
that has to be filled in (implemented) by subclasses.
152 17 Class definitions in TypeScript
Chapter 18
Class-related types
Contents
18.1 The two prototype chains of classes . . . . . . . . . . . . . . . . . . 153
18.2 Interfaces for instances of classes . . . . . . . . . . . . . . . . . . . . 154
18.3 Interfaces for classes . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
18.3.1 Example: converting from and to JSON . . . . . . . . . . . . . 155
18.3.2 Example: TypeScript’s built-in interfaces for the class Object
and for its instances . . . . . . . . . . . . . . . . . . . . . . . . 156
18.4 Classes as types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
18.4.1 Pitfall: classes work structurally, not nominally . . . . . . . . . 157
18.5 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
In this chapter about TypeScript, we examine types related to classes and their instances.
153
154 18 Class-related types
// Static method
const myCounter = Counter.createZero();
assert.ok(myCounter instanceof Counter);
assert.equal(myCounter.value, 0);
// Instance method
myCounter.increment();
assert.equal(myCounter.value, 1);
Object Object.prototype
··· ···
Counter Counter.prototype
__proto__ __proto__
prototype increment
create ··· constructor
myCounter
__proto__
value 0
Figure 18.1: Objects created by class Counter. Left-hand side: the class and its superclass
Object. Right-hand side: The instance myCounter, the prototype properties of Counter,
and the prototype methods of the superclass Object..
The diagram in fig. 18.1 shows the runtime structure of class Counter. There are two
prototype chains of objects in this diagram:
• Class (left-hand side): The static prototype chain consists of the objects that make
up class Counter. The prototype object of class Counter is its superclass, Object.
• Instance (right-hand side): The instance prototype chain consists of the objects that
make up the instance myCounter. The chain starts with the instance myCounter
and continues with Counter.prototype (which holds the prototype methods of
class Counter) and Object.prototype (which holds the prototype methods of class
Object).
In this chapter, we’ll first explore instance objects and then classes as objects.
increment(): void;
}
Structural interfaces are convenient because we can create interfaces even for objects that
already exist (i.e., we can introduce them after the fact).
If we know ahead of time that an object must implement a given interface, it often makes
sense to check early if it does, in order to avoid surprises later. We can do that for in-
stances of classes via implements:
class Counter implements CountingService {
// ···
};
Comments:
• TypeScript does not distinguish between inherited properties (such as .increment)
and own properties (such as .value).
• As an aside, private properties are ignored by interfaces and can’t be specified via
them. This is expected given that private data is for internal purposes only.
This is how we can check right away if class Person (as an object) implements the inter-
face JsonStatic:
The following way of making this check may seem like a good idea:
• We can’t new-call Person because JsonStatic does not have a construct signature.
• If Person has static properties beyond .fromJson(), TypeScript won’t let us access
them.
/**
* Provides functionality common to all JavaScript objects.
*/
declare var Object: ObjectConstructor;
interface ObjectConstructor {
new(value?: any): Object;
(): any;
(value: any): any;
/**
* Returns the prototype of an object.
* @param o The object that references the prototype.
*/
getPrototypeOf(o: any): any;
class Color {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
Why doesn’t TypeScript complain in line A? That’s due to structural typing: Instances of
Person and of Color have the same structure and are therefore statically compatible.
We can make the two groups of objects incompatible by adding private properties:
class Color {
name: string;
private branded = true;
constructor(name: string) {
this.name = name;
}
}
class Person {
name: string;
private branded = true;
constructor(name: string) {
this.name = name;
}
}
Contents
19.1 Types for specific classes . . . . . . . . . . . . . . . . . . . . . . . . . 161
19.2 The type operator typeof . . . . . . . . . . . . . . . . . . . . . . . . 162
19.2.1 Constructor type literals . . . . . . . . . . . . . . . . . . . . . 162
19.2.2 Object type literals with construct signatures . . . . . . . . . . 162
19.3 A generic type for classes: Class<T> . . . . . . . . . . . . . . . . . . 163
19.3.1 Example: creating instances . . . . . . . . . . . . . . . . . . . 163
19.3.2 Example: casting with runtime checks . . . . . . . . . . . . . 163
19.3.3 Example: Maps that are type-safe at runtime . . . . . . . . . . 164
19.3.4 Pitfall: Class<T> does not match abstract classes . . . . . . . . 165
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
161
162 19 Types for classes as values
What type should we use for the parameter PointClass if we want it to be Point or a
subclass?
// %inferred-type: Point
const point = createPoint(Point, 3, 6);
assert.ok(point instanceof Point);
Similarly, construct signatures enable interfaces and OLTs to describe constructor func-
tions. They look like call signatures with the added prefix new. In the next example,
PointClass has an object literal type with a construct signature:
function createPoint(
PointClass: {new (x: number, y: number): Point},
x: number, y: number
) {
return new PointClass(x, y);
}
class Person {
constructor(public name: string) {}
}
// %inferred-type: Person
const jane = createInstance(Person, 'Jane');
return obj;
}
With cast(), we can change the type of a value to something more specific. This is also
safe at runtime, because we both statically change the type and perform a dynamic check.
The following code provides an example:
class TypeSafeMap {
#data = new Map<any, any>();
get<T>(key: Class<T>) {
const value = this.#data.get(key);
return cast(key, value);
}
set<T>(key: Class<T>, value: T): this {
cast(key, value); // runtime check
this.#data.set(key, value);
return this;
}
has(key: any) {
return this.#data.has(key);
}
}
The key of each entry in a TypeSafeMap is a class. That class determines the static type of
the entry’s value and is also used for checks at runtime.
map.set(RegExp, /abc/);
// %inferred-type: RegExp
const re = map.get(RegExp);
Why is that? The rationale is that constructor type literals and construct signatures
should only be used for values that can actually be new-invoked (GitHub issue with more
information).
This is a workaround:
type Class2<T> = Function & {prototype: T};
Typing Arrays
Contents
20.1 Roles of Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
20.2 Ways of typing Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . 168
20.2.1 Array role “list”: Array type literals vs. interface type Array . 168
20.2.2 Array role “tuple”: tuple type literals . . . . . . . . . . . . . . 168
20.2.3 Objects that are also Array-ish: interfaces with index signatures 168
20.3 Pitfall: type inference doesn’t always get Array types right . . . . . 169
20.3.1 Inferring types of Arrays is difficult . . . . . . . . . . . . . . . 169
20.3.2 Type inference for non-empty Array literals . . . . . . . . . . 169
20.3.3 Type inference for empty Array literals . . . . . . . . . . . . . 170
20.3.4 const assertions for Arrays and type inference . . . . . . . . . 171
20.4 Pitfall: TypeScript assumes indices are never out of bounds . . . . . 172
• Lists: All elements have the same type. The length of the Array varies.
• Tuple: The length of the Array is fixed. The elements do not necessarily have the
same type.
TypeScript accommodates these two roles by offering various ways of typing arrays. We
will look at those next.
167
168 20 Typing Arrays
An Array type literal is a shorthand for using the global generic interface type Array:
If the element type is more complicated, we need parentheses for Array type literals:
(number|string)[]
(() => boolean)[]
Array<number|string>
Array<() => boolean>
20.2.3 Objects that are also Array-ish: interfaces with index signatures
If an interface has only an index signature, we can use it for Arrays:
interface StringArray {
[index: number]: string;
}
const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];
An interface that has both an index signature and property signatures, only works for
objects (because indexed elements and properties need to be defined at the same time):
interface FirstNamesAndLastName {
[index: number]: string;
lastName: string;
}
What is the best type for fields? The following are all reasonable choices:
type Fields = [
[string, string, boolean],
[string, string, boolean],
[string, string, boolean],
];
type Fields = [
[string, 'string', boolean],
[string, 'string', boolean],
[string, 'number', boolean],
];
type Fields = [
Array<string|boolean>,
Array<string|boolean>,
Array<string|boolean>,
];
}
// %inferred-type: number[]
const pair1 = [1, 2];
We can fix this by adding a type annotation to the const declaration, which avoids type
inference:
// %inferred-type: any[]
const arr1 = [];
arr1.push(123);
// %inferred-type: number[]
arr1;
arr1.push('abc');
// %inferred-type: (string | number)[]
arr1;
Note that the initial inferred type isn’t influenced by what happens later.
// %inferred-type: any[]
const arr1 = [];
arr1[0] = 123;
// %inferred-type: number[]
arr1;
arr1[1] = 'abc';
// %inferred-type: (string | number)[]
arr1;
In contrast, if the Array literal has at least one element, then the element type is fixed and
doesn’t change later:
// %inferred-type: number[]
const arr = [123];
We are declaring that rockCategories won’t change. That has the following effects:
• The Array becomes readonly – we can’t use operations that change it:
// %inferred-type: string[]
const rockCategories2 = ['igneous', 'metamorphic', 'sedimentary'];
• TypeScript infers literal types ("igneous" etc.) instead of more general types. That
is, the inferred tuple type is not [string, string, string].
Here are more examples of Array literals with and without const assertions:
First, the inferred type is as narrow as possible. That causes an issue for let-declared
variables: We cannot assign any tuple other than the one that we used for intialization:
That is neither an upside nor a downside, but we need to be aware that it happens.
// %inferred-type: string
const message = messages[3]; // (A)
Due to this assumption, the type of message is string. And not undefined or unde-
fined|string, as we may have expected.
as const would have had the same effect because it leads to a tuple type being inferred.
Chapter 21
Typing functions
Contents
21.1 Defining statically typed functions . . . . . . . . . . . . . . . . . . . 174
21.1.1 Function declarations . . . . . . . . . . . . . . . . . . . . . . . 174
21.1.2 Arrow functions . . . . . . . . . . . . . . . . . . . . . . . . . 174
21.2 Types for functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
21.2.1 Function type signatures . . . . . . . . . . . . . . . . . . . . . 174
21.2.2 Interfaces with call signatures . . . . . . . . . . . . . . . . . . 175
21.2.3 Checking if a callable value matches a function type . . . . . . 175
21.3 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
21.3.1 When do parameters have to be type-annotated? . . . . . . . . 176
21.3.2 Optional parameters . . . . . . . . . . . . . . . . . . . . . . . 177
21.3.3 Rest parameters . . . . . . . . . . . . . . . . . . . . . . . . . . 179
21.3.4 Named parameters . . . . . . . . . . . . . . . . . . . . . . . . 179
21.3.5 this as a parameter (advanced) . . . . . . . . . . . . . . . . . 180
21.4 Overloading (advanced) . . . . . . . . . . . . . . . . . . . . . . . . . 181
21.4.1 Overloading function declarations . . . . . . . . . . . . . . . . 181
21.4.2 Overloading via interfaces . . . . . . . . . . . . . . . . . . . . 183
21.4.3 Overloading on string parameters (event handling etc.) . . . . 183
21.4.4 Overloading methods . . . . . . . . . . . . . . . . . . . . . . 183
21.5 Assignability (advanced) . . . . . . . . . . . . . . . . . . . . . . . . 184
21.5.1 The rules for assignability . . . . . . . . . . . . . . . . . . . . 185
21.5.2 Consequences of the assignment rules for functions . . . . . . 185
21.6 Further reading and sources of this chapter . . . . . . . . . . . . . . 186
173
174 21 Typing functions
• Return value: By default, the return type of functions is inferred. That is usually
good enough. In this case, we opted to explicitly specify that repeat1() has the
return type string (last type annotation in line A).
The name of this type of function is Repeat. Among others, it matches all functions with:
• Two parameters whose types are string and number. We need to name parame-
ters in function type signatures, but the names are ignored when checking if two
function types are compatible.
• The return type string. Note that this time, the type is separated by an arrow and
can’t be omitted.
21.2 Types for functions 175
This type matches more functions. We’ll learn which ones, when we explore the rules
for assignability later in this chapter.
interface Repeat {
(str: string, times: number): string; // (A)
}
Note:
On one hand, interfaces are more verbose. On the other hand, they let us specify proper-
ties of functions (which is rare, but does happen):
interface Incrementor1 {
(x: number): number;
increment: number;
}
We can also specify properties via an intersection type (&) of a function signature type
and an object literal type:
type Incrementor2 =
(x: number) => number
& { increment: number }
;
If we declare a variable via const, we can perform the check via a type annotation:
Note that we don’t need to specify the type of parameter str because TypeScript can use
StringPredicate to infer it.
176 21 Typing functions
The following solution is slightly over the top (i.e., don’t worry if you don’t fully under-
stand it), but it demonstrates several advanced features:
• Parameters: We use Parameters<> to extract a tuple with the parameter types. The
three dots declare a rest parameter, which collects all parameters in a tuple/Array.
[str] destructures that tuple. (More on rest parameters later in this chapter.)
21.3 Parameters
21.3.1 When do parameters have to be type-annotated?
Recap: If --noImplicitAny is switched on (--strict switches it on), the type of each
parameter must either be inferrable or explicitly specified.
In the following example, TypeScript can’t infer the type of str and we must specify it:
In line A, TypeScript can use the type StringMapFunction to infer the type of str and
we don’t need to add a type annotation:
Here, TypeScript can use the type of .map() to infer the type of str:
assert.deepEqual(
['a', 'b', 'c'].map((str) => str + str),
['aa', 'bb', 'cc']);
interface Array<T> {
map<U>(
callbackfn: (value: T, index: number, array: T[]) => U,
thisArg?: any
): U[];
// ···
}
If we put a question mark after the name of a parameter, that parameter becomes optional
and can be omitted when calling the function:
function trim1(str?: string): string {
// Internal type of str:
// %inferred-type: string | undefined
str;
assert.equal(
trim1(), '');
The only way in which trim2() is different from trim1() is that the parameter can’t be
omitted in function calls (line A). In other words: We must be explicit when omitting a
parameter whose type is undefined|T.
assert.equal(
trim2('\n abc \t'), 'abc');
assert.equal(
trim2(undefined), ''); // OK!
If we specify a parameter default value for str, we don’t need to provide a type annota-
tion because TypeScript can infer the type:
return str.trim();
}
Note that the internal type of str is string because the default value ensures that it is
never undefined.
assert.equal(
trim3('\n abc \t'), 'abc');
21.3 Parameters 179
A rest parameter collects all remaining parameters in an Array. Therefore, its static type
is usually an Array. In the following example, parts is a rest parameter:
• We can use tuple types such as [string, number] for rest parameters.
• We can destructure rest parameters (not just normal parameters).
assert.equal(
padStart({str: '7', len: 3, fillStr: '0'}),
'007');
In plain JavaScript, functions can use destructuring to access named parameter values.
Alas, in TypeScript, we additionally have to specify a type for the object literal and that
leads to redundancies:
function padStart({ str, len, fillStr = ' ' } // (A)
: { str: string, len: number, fillStr: string }) { // (B)
return str.padStart(len, fillStr);
}
Note that the destructuring (incl. the default value for fillStr) all happens in line A,
while line B is exclusively about TypeScript.
It is possible to define a separate type instead of the inlined object literal type that we
have used in line B. However, in most cases, I prefer not to do that because it slightly
goes against the nature of parameters which are local and unique per function. If you
prefer having less stuff in function heads, then that’s OK, too.
interface Customer {
id: string;
fullName: string;
}
const jane = {id: '1234', fullName: 'Jane Bond'};
const lars = {id: '5678', fullName: 'Lars Croft'};
const idToCustomer = new Map<string, Customer>([
['1234', jane],
['5678', lars],
]);
assert.equal(
getFullName(idToCustomer, '1234'), 'Jane Bond'); // (A)
assert.equal(
getFullName(lars), 'Lars Croft'); // (B)
function getFullName(
customerOrMap: Customer | Map<string, Customer>,
id?: string
): string {
if (customerOrMap instanceof Map) {
if (id === undefined) throw new Error();
const customer = customerOrMap.get(id);
if (customer === undefined) {
182 21 Typing functions
However, with this type signature, function calls are legal at compile time that produce
runtime errors:
• The actual implementation starts in line C. It is the same as in the previous exam-
ple.
• In line A and line B there are the two type signatures (function heads without
bodies) that can be used for getFullName(). The type signature of the actual im-
plementation cannot be used!
My advice is to only use overloading when it can’t be avoided. One alternative is to split
an overloaded function into multiple functions with different names – for example:
• getFullName()
• getFullNameViaMap()
21.4 Overloading (advanced) 183
In this case, it is relatively difficult to get the types of the implementation (starting in line
A) right, so that the statement in the body (line B) works. As a last resort, we can always
use the type any.
class StringBuilder {
#data = '';
toString() {
return this.#data;
}
}
interface ArrayConstructor {
from<T>(arrayLike: ArrayLike<T>): T[];
from<T, U>(
arrayLike: ArrayLike<T>,
mapfn: (v: T, k: number) => U,
thisArg?: any
): U[];
}
• In the first signature, the returned Array has the same element type as the param-
eter.
• In the second signature, the elements of the returned Array have the same
type as the result of mapfn. This version of Array.from() is similar to Ar-
ray.prototype.map().
of type Trg?
Understanding assignability helps us answer questions such as:
• Given the function type signature of a formal parameter, which functions can be
passed as actual parameters in function calls?
• Given the function type signature of a property, which functions can be assigned
to it?
The following example demonstrates that if the target return type is void, then the source
return type doesn’t matter. Why is that? void results are always ignored in TypeScript.
const trg2: () => void = () => new Date();
186 21 Typing functions
The source must not have more parameters than the target:
// @ts-expect-error: Type '(x: string) => string' is not assignable to
// type '() => string'. (2322)
const trg3: () => string = (x: string) => 'abc';
Why is that? The target specifies the expectations for the source: It must accept the pa-
rameter x. Which it does (but it ignores it). This permissiveness enables:
['a', 'b'].map(x => x + x)
The callback for .map() only has one of the three parameters that are mentioned in the
type signature of .map():
map<U>(
callback: (value: T, index: number, array: T[]) => U,
thisArg?: any
): U[];
187
Chapter 22
Contents
22.1 Type assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
22.1.1 Alternative syntax for type assertions . . . . . . . . . . . . . . 190
22.1.2 Example: asserting an interface . . . . . . . . . . . . . . . . . 190
22.1.3 Example: asserting an index signature . . . . . . . . . . . . . 190
22.2 Constructs related to type assertions . . . . . . . . . . . . . . . . . . 191
22.2.1 Non-nullish assertion operator (postfix !) . . . . . . . . . . . . 191
22.2.2 Definite assignment assertions . . . . . . . . . . . . . . . . . . 192
This chapter is about type assertions in TypeScript, which are related to type casts in other
languages and performed via the as operator.
assert.equal(
(data as Array<string>).length, 3); // (C)
Comments:
189
190 22 Type assertions (related to casting)
• In line B, we see that this type doesn’t let us access any properties (details).
• In line C, we use a type assertion (the operator as) to tell TypeScript that data is
an Array. Now we can access property .length.
Type assertions are a last resort and should be avoided as much as possible. They (tem-
porarily) remove the safety net that the static type system normally gives us.
Note that, in line A, we also overrode TypeScript’s static type. But we did it via a type an-
notation. This way of overriding is much safer than type assertions because we are much
more constrained: TypeScript’s type must be assignable to the type of the annotation.
<Array<string>>data
I recommend avoiding this syntax. It has grown out of style and is not compatible with
React JSX code (in .tsx files).
interface Named {
name: string;
}
function getName(obj: object): string {
if (typeof (obj as Named).name === 'string') { // (A)
return (obj as Named).name; // (B)
}
return '(Unnamed)';
}
assert.equal(
theName!.length, 4); // OK
After we use the Map method .has(), we know that a Map has a given key. Alas, the
result of .get() does not reflect that knowledge, which is why we have to use the nullish
assertion operator:
We can avoid the nullish assertion operator whenever the values of a Map can’t be un-
defined. Then missing entries can be detected by checking if the result of .get() is
undefined:
return -1;
}
// %inferred-type: string
value;
return value.length;
}
constructor() {
this.initProperties();
}
initProperties() {
this.x = 0;
this.y = 0;
}
}
The errors go away if we use definite assignment assertions (exclamation marks) in line A
and line B:
class Point2 {
x!: number; // (A)
y!: number; // (B)
constructor() {
this.initProperties();
}
initProperties() {
this.x = 0;
this.y = 0;
}
}
Chapter 23
Contents
23.1 When are static types too general? . . . . . . . . . . . . . . . . . . . 194
23.1.1 Narrowing via if and type guards . . . . . . . . . . . . . . . 194
23.1.2 Narrowing via switch and a type guard . . . . . . . . . . . . 195
23.1.3 More cases of types being too general . . . . . . . . . . . . . . 195
23.1.4 The type unknown . . . . . . . . . . . . . . . . . . . . . . . . . 196
23.2 Narrowing via built-in type guards . . . . . . . . . . . . . . . . . . . 196
23.2.1 Strict equality (===) . . . . . . . . . . . . . . . . . . . . . . . . 196
23.2.2 typeof, instanceof, Array.isArray . . . . . . . . . . . . . . . 197
23.2.3 Checking for distinct properties via the operator in . . . . . . 197
23.2.4 Checking the value of a shared property (discriminated unions) 198
23.2.5 Narrowing dotted names . . . . . . . . . . . . . . . . . . . . . 199
23.2.6 Narrowing Array element types . . . . . . . . . . . . . . . . . 200
23.3 User-defined type guards . . . . . . . . . . . . . . . . . . . . . . . . 201
23.3.1 Example of a user-defined type guard: isArrayWithInstance-
sOf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
23.3.2 Example of a user-defined type guard: isTypeof() . . . . . . 202
23.4 Assertion functions . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
23.4.1 TypeScript’s support for assertion functions . . . . . . . . . . 204
23.4.2 Asserting a boolean argument: asserts «cond» . . . . . . . . 204
23.4.3 Asserting the type of an argument: asserts «arg» is «type» 205
23.5 Quick reference: user-defined type guards and assertion functions . 205
23.5.1 User-defined type guards . . . . . . . . . . . . . . . . . . . . 205
23.5.2 Assertion functions . . . . . . . . . . . . . . . . . . . . . . . . 206
23.6 Alternatives to assertion functions . . . . . . . . . . . . . . . . . . . 206
23.6.1 Technique: forced conversion . . . . . . . . . . . . . . . . . . 206
23.6.2 Technique: throwing an exception . . . . . . . . . . . . . . . . 207
193
194 23 Type guards and assertion functions
In TypeScript, a value can have a type that is too general for some operations – for exam-
ple, a union type. This chapter answers the following questions:
• What is narrowing of types?
– Spoiler: Narrowing means changing the static type T of a storage location
(such as a variable or a property) to a subset of T. For example, it is often
useful to narrow the type null|string to the type string.
• What are type guards and assertion functions and how can we use them to narrow
types?
– Spoiler: typeof and instanceof are type guards.
Inside the body of getScore(), we don’t know if the type of value number or string.
Before we do, we can’t really work with value.
In this chapter, we interpret types as sets of values. (For more information on this inter-
pretation and another one, see §11 “What is a type in TypeScript? Two perspectives”.)
23.1 When are static types too general? 195
Inside the then-blocks starting in line A and line B, the static type of value changes,
due to the checks we performed. We are now working with subsets of the original type
number|string. This way of reducing the size of a type is called narrowing. Checking the
result of typeof and similar runtime operations are called type guards.
Note that narrowing does not change the original type of value, it only makes it more
specific as we pass more checks.
• Nullable types:
• Discriminated unions:
In other words: The type unknown is too general and we must narrow it. In a way, unknown
is also a union type (the union of all types).
For some union types, we can use === to differentiate between their components:
interface Book {
title: null | string;
isbn: string;
}
}
}
Using === for including and !=== for excluding a union type component only works if
that component is a singleton type (a set with one member). The type null is a singleton
type. Its only member is the value null.
if (Array.isArray(value)) {
// %inferred-type: number[]
value;
}
}
Note how the static type of value is narrowed inside the then-blocks.
type FirstOrSecond =
| {first: string}
| {second: string};
// ···
}
}
The problem in this case is that, without narrowing, we can’t access property .second of
a value whose type is FirstOrSecond.
In the previous example, .kind is a discriminant: Each components of the union type
Attendee has this property, with a unique value.
23.2 Narrowing via built-in type guards 199
type MyType = {
prop?: number | string,
};
function func(arg: MyType) {
if (typeof arg.prop === 'string') {
// %inferred-type: string
arg.prop; // (A)
[].forEach((x) => {
// %inferred-type: string | number | undefined
arg.prop; // (B)
});
// %inferred-type: string
arg.prop;
arg = {};
If we use .every() to check that all Array elements are non-nullish, TypeScript does not
narrow the type of mixedValues (line A):
if (mixedValues.every(isNotNullish)) {
// %inferred-type: readonly (number | null | undefined)[]
mixedValues; // (A)
}
The previous code uses the following user-defined type guard (more on what that is soon):
NonNullable<Union> (line A) is a utility type that removes the types undefined and null
from union type Union.
23.2.6.2 The Array method .filter() produces Arrays with narrower types
.filter() produces Arrays that have narrower types (i.e., it doesn’t really narrow exist-
ing types):
// %inferred-type: number[]
const numbers = mixedValues.filter(isNotNullish);
Alas, we must use a type guard function directly – an arrow function with a type guard
is not enough:
The return type value is Function is a type predicate. It is part of the type signature of
isFunction():
A user-defined type guard must always return booleans. If isFunction(x) returns true,
TypeScript narrows the type of the actual argument x to Function:
Note that TypeScript doesn’t care how we compute the result of a user-defined type
guard. That gives us a lot of freedom w.r.t. the checks we use. For example, we could
have implemented isFunction() as follows:
Alas, we have to use the type any for the parameter value because the type unknown does
not let us make the function call in line A.
* anymore.
*/
function isArrayWithInstancesOf<T>(
arr: any, Class: new (...args: any[])=>T)
: arr is Array<T>
{
if (!Array.isArray(arr)) {
return false;
}
if (!arr.every(elem => elem instanceof Class)) {
return false;
}
// %inferred-type: any[]
arr; // (A)
return true;
}
In line A, we can see that the inferred type of arr is not Array<T>, but our checks have
ensured that it currently is. That’s why we can return true. TypeScript trusts us and
narrows to Array<T> when we use isArrayWithInstancesOf():
/**
* An implementation of the `typeof` operator.
*/
function isTypeof<T>(value: unknown, prim: T): value is T {
if (prim === null) {
return value === null;
}
return value !== null && (typeof prim) === (typeof value);
}
Ideally, we’d be able to specify the expected type of value via a string (i.e., one of the
results of typeof). But then we would have to derive the type T from that string and
it’s not immediately obvious how to do that (there is a way, as we’ll see soon). As a
workaround, we specify T via a member prim of T:
if (isTypeof(value, 123)) {
// %inferred-type: number
value;
}
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof(value: any, typeString: 'boolean'): value is boolean;
function isTypeof(value: any, typeString: 'number'): value is number;
function isTypeof(value: any, typeString: 'string'): value is string;
function isTypeof(value: any, typeString: string): boolean {
return typeof value === typeString;
}
An alternative is to use an interface as a map from strings to types (several cases are
omitted):
interface TypeMap {
boolean: boolean;
number: number;
string: string;
}
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof<T extends keyof TypeMap>(value: any, typeString: T)
: value is TypeMap[T] {
return typeof value === typeString;
}
value;
}
On Node.js, assert() is supported via the built-in module assert. The following code
uses it in line A:
import assert from 'assert';
function removeFilenameExtension(filename: string) {
const dotIndex = filename.lastIndexOf('.');
assert(dotIndex >= 0); // (A)
return filename.slice(0, dotIndex);
}
// %inferred-type: Set<any>
value;
}
23.5 Quick reference: user-defined type guards and assertion functions 205
We are using the argument value instanceof Set similarly to a type guard, but instead
of skipping part of a conditional statement, false triggers an exception.
This time, calling the assertion function, narrows the type of its argument:
// %inferred-type: number
value;
}
The function addXY() adds properties to existing objects and updates their types accord-
ingly:
An intersection type S & T has the properties of both type S and type T.
Forced conversion is a versatile technique with uses beyond those of assertion functions.
For example, we can convert:
For more information, see §24.3.5 “External vs. internal representation of data”.
// %inferred-type: string
value; // after type check
return value.length;
}
return -1;
}
Instead of the if statement that starts in line A, we also could have used an assertion
function:
assertNotUndefined(value);
208 23 Type guards and assertion functions
Contents
24.1 JSON schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
24.1.1 An example JSON schema . . . . . . . . . . . . . . . . . . . . 210
24.2 Approaches for data validation in TypeScript . . . . . . . . . . . . . 211
24.2.1 Approaches not using JSON schema . . . . . . . . . . . . . . . 211
24.2.2 Approaches using JSON schema . . . . . . . . . . . . . . . . . 211
24.2.3 Picking a library . . . . . . . . . . . . . . . . . . . . . . . . . 211
24.3 Example: validating data via the library Zod . . . . . . . . . . . . . 212
24.3.1 Defining a “schema” via Zod’s builder API . . . . . . . . . . . 212
24.3.2 Validating data . . . . . . . . . . . . . . . . . . . . . . . . . . 212
24.3.3 Type guards . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
24.3.4 Deriving a static type from a Zod schema . . . . . . . . . . . . 213
24.3.5 External vs. internal representation of data . . . . . . . . . . . 213
24.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Data validation means ensuring that data has the desired structure and content.
With TypeScript, validation becomes relevant when we receive external data such as:
• Data parsed from JSON files
• Data received from web services
In these cases, we expect the data to fit static types we have, but we can’t be sure. Con-
trast that with data we create ourselves, where TypeScript continuously checks that ev-
erything is correct.
This chapter explains how to validate external data in TypeScript.
209
210 24 Validating external data
The idea behind JSON schema is to express the schema (structure and content, think static
type) of JSON data in JSON. That is, metadata is expressed in the same format as data.
• Validating JSON data: If we have a schema definition for data, we can use tools to
check that the data is correct. One issue with data can also be fixed automatically:
We can specify default values that can be used to add properties that are missing.
• Documenting JSON data formats: On one hand, the core schema definitions can be
considered documentation. But JSON schema additionally supports descriptions,
deprecation notes, comments, examples, and more. These mechanisms are called
annotations. They are not used for validation, but for documentation.
• IDE support for editing data: For example, Visual Studio Code supports JSON
schema. If there is a schema for a JSON file, we gain several editing features:
auto-completion, highlighting of errors, etc. Notably, VS Code’s support for pack-
age.json files is completely based on a JSON schema.
{
"$id": "https://example.com/geographical-location.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Longitude and Latitude Values",
"description": "A geographical coordinate.",
"required": [ "latitude", "longitude" ],
"type": "object",
"properties": {
"latitude": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"longitude": {
"type": "number",
"minimum": -180,
"maximum": 180
}
}
}
{
"latitude": 48.858093,
"longitude": 2.294694
}
24.2 Approaches for data validation in TypeScript 211
• If we are starting with TypeScript types and want to ensure that data (coming from
configuration files, etc.) fits those types, then builder APIs that support static types
are a good choice.
• If our starting point is a JSON schema, then we should consider one of the libraries
that support JSON schemas.
212 24 Validating external data
• If we are handling data that is more messy (e.g. submitted via forms), we may need
a more flexible approach where static types play less of a role.
For larger schemas, it can make sense to break things up into multiple const declarations.
Zod can produce a static type from FileEntryInputSchema, but I decided to (redun-
dantly!) manually maintain the static type FileEntryInput:
type FileEntryInput =
| string
| [string, string, string[]]
| {file: string, author?: string, tags?: string[]}
;
assert.throws(
() => validateData(['iceland.txt', 'me']));
The static type of the result of FileEntryInputSchema.parse() is what Zod derived from
FileEntryInputSchema. By making FileEntryInput the return type of validateData(),
we ensure that the former type is assignable to the latter.
It can make sense to define a custom type guard that supports FileEntryInput instead
of what Zod infers.
// %inferred-type: string
// | [string, string, string[]]
// | {
// author?: string | undefined;
// tags?: string[] | undefined;
// file: string;
// }
type FileEntryInputDerived = z.infer<typeof FileEntryInputSchema>;
On one hand, there is the type that describes the input data. Its structure is optimized
for being easy to author:
214 24 Validating external data
type FileEntryInput =
| string
| [string, string, string[]]
| {file: string, author?: string, tags?: string[]}
;
On the other hand, there is the type that is used in the program. Its structure is optimized
for being easy to use in code:
type FileEntry = {
file: string,
author: null|string,
tags: string[],
};
After we have used Zod to ensure that the input data conforms to FileEntryInput, we
use a conversion function that converts the data to a value of type FileEntry.
24.4 Conclusion
In the long run, I expect compile-time solutions that derive validation functions from
static types, to improve w.r.t. usability. That should make them more popular.
For libraries that have builder APIs, I’d like to have tools that compile TypeScript types
to builder API invocations (online and via a command line). This would help in two
ways:
• The tools can be used to explore how the APIs work.
• We have the option of producing API code via the tools.
Part VI
Miscellaneous
215
Chapter 25
Contents
25.1 Types as metavalues . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
25.2 Generic types: factories for types . . . . . . . . . . . . . . . . . . . . 218
25.3 Union types and intersection types . . . . . . . . . . . . . . . . . . . 219
25.3.1 Union types (|) . . . . . . . . . . . . . . . . . . . . . . . . . . 219
25.3.2 Intersection types (&) . . . . . . . . . . . . . . . . . . . . . . . 220
25.4 Control flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
25.4.1 Conditional types . . . . . . . . . . . . . . . . . . . . . . . . . 221
25.4.2 Mapped types . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
25.5 Various other operators . . . . . . . . . . . . . . . . . . . . . . . . . 225
25.5.1 The index type query operator keyof . . . . . . . . . . . . . . 225
25.5.2 The indexed access operator T[K] . . . . . . . . . . . . . . . . 226
25.5.3 The type query operator typeof . . . . . . . . . . . . . . . . . 227
In this chapter, we explore how we can compute with types at compile time in TypeScript.
Note that the focus of this chapter is on learning how to compute with types. Therefore,
we’ll use literal types a lot and the examples are less practically relevant.
217
218 25 An overview of computing with types
What does it mean that we can compute with types? The following code is an example:
type ObjectLiteralType = {
first: 1,
second: 2,
};
interface InterfaceType {
prop1: string;
prop2: number;
}
The generic type Wrap<> has the parameter T. Its result is T, wrapped in a tuple type. This
is how we use this metafunction:
// %inferred-type: [string]
type Wrapped = Wrap<string>;
We pass the parameter string to Wrap<> and give the result the alias Wrapped. The result
is a tuple type with a single component – the type string.
If we view type A and type B as sets, then A | B is the set-theoretic union of these sets.
Put differently: The members of the result are members of at least one of the operands.
Syntactically, we can also put a | in front of the first component of a union type. That is
convenient when a type definition spans multiple lines:
type A =
| 'a'
| 'b'
| 'c'
;
type Obj = {
first: 1,
second: 2,
};
We’ll soon see type-level operations for looping over such collections.
220 25 An overview of computing with types
Due to each member of a union type being a member of at least one of the component
types, we can only safely access properties that are shared by all component types (line
A). To access any other property, we need a type guard (line B):
type ObjectTypeA = {
propA: bigint,
sharedProp: string,
}
type ObjectTypeB = {
propB: boolean,
sharedProp: string,
}
// boolean
arg.propB;
}
}
If we view type A and type B as sets, then A & B is the set-theoretic intersection of these
sets. Put differently: The members of the result are members of both operands.
The intersection of two object types has the properties of both types:
type Obj1 = { prop1: boolean };
type Obj2 = { prop2: number };
25.4 Control flow 221
type Both = {
prop1: boolean,
prop2: number,
};
If we are mixin in an object type Named into another type Obj, then we need an intersection
type (line A):
interface Named {
name: string;
}
function addName<Obj extends object>(obj: Obj, name: string)
: Obj & Named // (A)
{
const namedObj = obj as (Obj & Named);
namedObj.name = name;
return namedObj;
}
const obj = {
last: 'Doe',
};
If Type2 is assignable to Type1, then the result of this type expression is ThenType. Oth-
erwise, it is ElseType.
25.4.1.1 Example: only wrapping types that have the property .length
In the following example, Wrap<> only wraps types in one-element tuples if they have
the property .length whose values are numbers:
222 25 An overview of computing with types
// %inferred-type: [string]
type A = Wrap<string>;
// %inferred-type: RegExp
type B = Wrap<RegExp>;
For more information on the type relationship assignability, see §11 “What is a type in
TypeScript? Two perspectives”.
Conditional types are distributive: Applying a conditional type C to a union type U is the
same as the union of applying C to each component of U. This is an example:
// Equivalent:
type C2 = Wrap<boolean> | Wrap<string> | Wrap<number[]>;
In other words, distributivity enables us to “loop” over the components of a union type.
25.4.1.4 With distributive conditional types, we use type never to ignore things
This is what happens if we swap the type expressions of the then-branch and the else-
branch:
// %inferred-type: 1 | 2
type Result2 = KeepNumbers<1 | 'a' | 2 | 'b'>;
Excluding types from a union is such a common operation that TypeScript provides the
built-in utility type Exclude<T, U>:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
// %inferred-type: "a" | 2
type Result2 = Exclude<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
// %inferred-type: 1 | 2
type Result1 = Extract<1 | 'a' | 2 | 'b', number>;
// %inferred-type: 1 | "b"
type Result2 = Extract<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
224 25 An overview of computing with types
Similarly to JavaScript’s ternary operator, we can also chain TypeScript’s conditional type
operator:
type LiteralTypeName<T> =
T extends undefined ? "undefined" :
T extends null ? "null" :
T extends boolean ? "boolean" :
T extends number ? "number" :
T extends bigint ? "bigint" :
T extends string ? "string" :
never;
// %inferred-type: "bigint"
type Result1 = LiteralTypeName<123n>;
The operator in is a crucial part of a mapped type: It specifies where the keys for the new
object literal type come from.
The following built-in utility type lets us create a new object by specifying which prop-
erties of an existing object type we want to keep:
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
It is used as follows:
type ObjectLiteralType = {
eeny: 1,
meeny: 2,
miny: 3,
moe: 4,
};
25.5 Various other operators 225
The following built-in utility type lets us create a new object type by specifying which
properties of an existing object type we want to omit:
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Explanations:
• K extends keyof any means that K must be a subtype of the type of all property
keys:
• Exclude<keyof T, K>> means: take the keys of T and remove all “values” men-
tioned in K.
type ObjectLiteralType = {
eeny: 1,
meeny: 2,
miny: 3,
moe: 4,
};
type Obj = {
0: 'a',
1: 'b',
prop0: 'c',
prop1: 'd',
};
226 25 An overview of computing with types
Applying keyof to a tuple type has a result that may be somewhat unexpected:
// number | "0" | "1" | "2" | "length" | "pop" | "push" | ···
type Result = keyof ['a', 'b', 'c'];
// %inferred-type: "shared"
type Result2 = keyof (A | B);
This makes sense if we remember that A & B has the properties of both type A and type B.
A and B only have property .shared in common, which explains Result2.
The type in brackets must be assignable to the type of all property keys (as computed by
keyof). That’s why Obj[number] and ‘Obj[string] are not allowed. However, we can
use number and string as index types if the indexed type has an index signature (line
A):
type Obj = {
[key: string]: RegExp, // (A)
};
// %inferred-type: RegExp
type ValuesOfObj = Obj[string];
KeysOfObj includes the type number because number keys are a subset of string keys in
JavaScript (and therefore in TypeScript).
// %inferred-type: 1 | 2 | 3
type Result1 = MyType['prop'];
// Equivalent:
type Result2 =
| { prop: 1 }['prop']
| { prop: 2 }['prop']
| { prop: 3 }['prop']
;
// %inferred-type: "abc"
type Result = typeof str;
228 25 An overview of computing with types
The first 'abc' is a value, while the second "abc" is its type, a string literal type.
This is another example of using typeof:
const func = (x: number) => x + x;
// %inferred-type: (x: number) => number
type Result = typeof func;
§15.1.2 “Adding a symbol to a type” describes an interesting use case for typeof.