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).
let b = 5678; // type = number
const c = 5678; // type = 5678
For example:
let oneMillion = 1_000_0000;
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).
let a: {
b: number;
c?: string; // optional property
[key: number]: boolean; // index signature
};
a = { b: 10, 1: true, 100: false }; // example
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.
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
: 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)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.
A parameter is the data needed by the function to run. An argument is the data you pass to the function when invoking it.
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.
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
}
};
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
};
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;
}
Functions that take any number of arguments.
A style of programming where you sketch out type signatures first and fill in values later.
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);
}
}
constructor
function to this
to make them part of the class. You can use the keywords private
, public
and private
for that (i.e. less typing).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.
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;
};
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.
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