Reading notes Programming TypeScript
Koen van Gilst / August 11, 2019
7 min read • ––– views
While on holiday I read Programming TypeScript
by Boris Cherny. It's excellent. Below you'll find some of my reading notes (stuff I don't want to forget).
Type literals
let b = 5678; // type = number
const c = 5678; // type = 5678
Numeric separator
For example:
let oneMillion = 1_000_0000;
Structurally typed
JavaScript (and TypeScript) are structurally typed. This means that it's not the name of the object that determines its type (this would be nominally typed) but the properties that the object has. Structural typing is also known as duck typing (i.e. if it walks and swims like a duck, the object is of the type duck).
Index signatures
let a: {
b: number;
c?: string; // optional property
[key: number]: boolean; // index signature
};
a = { b: 10, 1: true, 100: false }; // example
Tuples
Tuples are a subtype of an array. They've got a fixed length and each index has a known type. For example:
let b: [string, string, number] = ['koen', 'van gilst', 1978];
Cherny advises using tuples often: they allow you to safely encode heterogeneous lists with fixed lengths.
Immutable arrays
let as: readonly number[] = [1, 2, 3];
This could be very useful in a React/Redux context: To avoid accidental mutations of the state object.
Undefined
undefined
: variable is not defined (yet)null
: variable has no valuevoid
: return type of function that does not return anythingnever
: return type of function that never returns (exception or infinite loop)
Enums
Cherny explains that it's best to stay away from Enums in TypeScript, because of all the pitfalls. There are plenty of other ways to express yourself.
Parameter vs argument
A parameter is the data needed by the function to run. An argument is the data you pass to the function when invoking it.
Typing functions
type Log = (msg: string, userId?: string) => void;
let log: Log = (msg, userId = 'Not signed in') => {
console.log(msg + userId);
};
Contextual typing infers from the context that msg
has to be a string.
Function overloading
Let's a function do two different things based on the call signature. For example:
type Reserve = {
(from: Date, to: Date, dest: string): Reservation;
(from: Date, dest: string): Reservation;
};
let reserve: Reserve = (
from: Date,
toOrDestination: Date | string,
destination?: string
) => {
if (toOrDestination instanceof Date && destination !== undefined) {
// Book a one-way trip
} else if (typeof toOrDestination === 'string') {
// Book a round trip
}
};
Typing properties on functions
Consider the following code which assigns a property wasCalled
to the function warnUser
function warnUser(warning) {
if (warnUser.wasCalled) {
return;
}
warnUser.wasCalled = true;
alert(warning);
}
warnUser.wasCalled = false;
This can be typed with TypeScript as follows:
type WarnUser = {
(warning: string): void; // the function
wasCalled: boolean; // property
};
Generics
With generics, we can keep type constraints on functions even if we don't know exactly what the type of the variable is going to be when we invoke the function. For example, this is what a typed version of the array filter function would look like:
filter([ 1 , 2 , 3 , 4 ], el => el < 3) // example usage, evaluates to [1, 2]
type Filter = {
<T>(array: T[], f: (item: T) = > boolean): T[]
}
Here we're defining a generic
called T
. Then we say that the function filter
expects an array of elements with type T
(could be a string, boolean or some object) and a function with an element of type T
as a parameter and a return value of type boolean
. The result of the function is, again, an array of elements with type T
.
Here's another example from the book. This is the typing for the map
function, using two generics T
and U
.
function map<T, U>(array: T[], f: (item: T) => U): U[] {
let result = [];
for (let i = 0; i < array.length; i++) {
result[i] = f(array[i]);
}
return result;
}
Variadic functions
Functions that take any number of arguments.
Type-driven development
A style of programming where you sketch out type signatures first and fill in values later.
Private vs protected
Consider the following code:
type Color = 'Black' | 'White';
type File = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H';
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
class Position {
constructor(private file: File, private rank: Rank) {}
}
class Piece {
protected position: Position;
constructor(private readonly color: Color, file: File, rank: Rank) {
this.position = new Position(file, rank);
}
}
- You don't have to reassign arguments in the
constructor
function tothis
to make them part of the class. You can use the keywordsprivate
,public
andprivate
for that (i.e. less typing). - Private means "that code within a Piece instance can read and write to it, but code outside of a Piece instance can’t. Different instances of Piece can access each other’s private members; instances of any other class - even a subclass of Piece - can’t."
- Protected is similar to private, but it "makes the property visible both to instances of Piece and to instances of any subclass of Piece."
- A public property is accessible from anywhere.
Abstracts
Using the abstract
keyword in front of a class means that you can't instantiate that class directly, you first have to extend it using a new class.
// ...
abstract class Piece {
// ...
moveTo(position: Position) {
this.position = position;
}
abstract canMoveTo(position: Position): boolean;
}
Using the abstract
keyword in front of a class method tells any class that extends Piece
that they should implement a canMoveTo
method with that signature.
Interfaces
Like aliases interfaces let you define types of things. Interfaces don’t have to extend other interfaces. An interface can extend any shape: an object type, a class, or another interface.
type Cake = {
calories: number;
sweet: boolean;
tasty: boolean;
};
Decorators
Are similar to Higher Order Components in React: they work like functions that change (decorate) the behavior of the class you're decorating. They offer a succinct syntax to do this using:
@serializable
class APIPayload {
getValue(): Payload {
// ...
}
}
Which would be equivalent to:
let APIPayload = serializable(
class APIPayload {
getValue(): Payload {
// ...
}
}
);
However, since decorators are still experimental in JavaScript (and also in TypeScript) (see: https://github.com/tc39/proposal-decorators) Cherny recommends sticking to ordinary functions until decorators become stable.
Builder pattern
The builder pattern is a commonly used API style in JavaScript that looks like this:
new RequestBuilder()
.setURL('/users')
.setMethod('get')
.setData({ firstName: 'Anna' })
.send();
Cherny shows how to build this in a type-safe way using TypeScript:
class RequestBuilder {
private data: object | null = null;
private method: 'get' | 'post' | null = null;
private url: string | null = null;
setMethod(method: 'get' | 'post'): this {
this.method = method;
return this;
}
setData(data: object): this {
this.data = data;
return this;
}
setURL(url: string): this {
this.url = url;
return this;
}
send() {
// ...
}
}
All quotations from Programming TypeScript: Making Your JavaScript Applications Scale