TypeScript Handbook Summary

Simon
8 min readMay 4, 2020

This is an abridged (and slightly opinionated) version of TypeScrip’s official handbook .

This is JavaScript

TypeScript may look like C# or Java but make no mistake, this is JavaScript. Anything you can do in JS you can also do it in TS. If you’re coming from an OOP language thinking you can use TypeScript to avoid JavaScript then I got bad news for you.

This reminds me a lot about CoffeeScript. A long time ago (in IT years) a lot of people, myself included, used Coffeescript to avoid JavaScript. That was the bargaining phase on the way to JS. Just accept and embrace it. Yeah, it has its quirks but it also offers a ton of useful things other languages don’t.

Use and abuse the types

The more information you give the compiler about your types the better.

Have you ever heard of the programming language Elm? It is famous for having virtually zero runtime errors. How they do it? Types aren’t optional. You can’t access an undefined property without dealing with the possible values first.

TypeScript catches many potential problems but it still need your help. Use the types, specially for function parameters, and you’ll reduce the number of runtime exceptions in your applications.

Basic Types

Arrays

The preferred way to declare an array is with a constant and the generic type ReadonlyArray.

const customers: ReadonlyArray<Customer> = [];

TypeScript already uses brackets for a lot of things so the generic version is more readable. It’s always good practice to use const unless you know you need a let. And read only is to fake immutability =)

Immutable arrays

Not 100% bullet proof immutable (remember, this is JS) but close enough. Most of the time you don’t need to mutate arrays. For example, instead of looping through an array only to create another, just use map into a new constant.

Here’s a readOnly helper function to turn any map into a type safe read only array. The resulting array is read only and typed regardless of map projection.

function readOnly<T>(array : Array<T>) : ReadonlyArray<T> {
return array;
}
const source: ReadonlyArray<number> = [1,2,3];
const result = readOnly(source.map(n => ({ num: n })));

Tuples

Favor objects over tuples. They’re easier to read and maintain. Tuples are kind of typed arrays where you access the values by index, not property name.

const values = { age: 21, name: "Alan", coder: true };
const name = values.name;

Tuples allow you to have a reference to multiple values. This similar to having an anonymous record in a DB. The fields are typed but they don’t belong in any particular table.

const values: [number, string, boolean] = [21, "Alan", true];
const name = values[1];

Enums

You can do more than just group values, you can safely map enums to any type of value(s):

enum Color { Red, Green };
const colorHex: { [key in Color]: string} = {
[Color.Red]: "FF0000",
[Color.Green]: "00FF00"
}

The advantage here is that you’ll get a compile error if a new color is added to the enum but not to the color hex map.

Any

Avoid any, implicit or explicit.
If you're getting an object from an external untyped source then create a type for the shape of the object. Watch out for implicit any’s for function parameters. If there’s one place to *really* use types is for parameters.

Destructuring

You can extract values from arrays and objects and put the values in variables as you declare them.

From array

const [first, second] = [1, 2];
// first === 1
// second === 2

Destructuring also works to assign values:

// swap variables
[first, second] = [second, first];

From object

You can extract object properties into variables/constants.

const person = {
name: "Alan",
age: 21
}
const { age } = person;
const { age: personAge } = person; // Use a different variable name

Spreads

In TS/JS the symbol ... basically means "and the rest of". It works with arrays or objects.

With Arrays

The following is an example of using spreads to accept an array of values. Notice it takes one parameter (an array of number), but there’s something peculiar about the way the array is declared. The first item in the array is called num and the second one ...rest .

That means the array will be typed (only accepting numbers), the first element/number in the array will go to the variable num “and the rest” will go to the array called rest.

const addArray = ([num, ...rest]: Array<number>): number => {
if (num === undefined)
return 0
else
return num + addArray(rest)
}
addArray([1, 2, 3]); //=> 6
addArray(["", 4]); // error

You can use the same trick to (shallow) clone an array:
const newArr = [ ...oldArr ];

Function parameters

If you use a spread for function parameters then it becomes “and the rest of the parameters”. In the following example addNums takes two parameters (numand rest) but we can call it with any number of parameters (as long as they’re numbers). The first parameter will go to num and the rest will go into the array rest.

const addNums = (num: number, ...rest: Array<number>): number => {
if (num === undefined)
return 0
else
return num + addNums(rest[0], ...rest.slice(1))
}
addNums(1, 2, 3); //=> 6
addNums("", 4); // error

Objects

You can use spread operators to combine multiple objects:

const defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
const search = { ...defaults, food: "rich" }; // Order is important!
// Extending objects (no design patterns required)const duck = {
quack() {}
}
const flyer = {
fly() { }
}
const flyingDuck = { ...duck, ...flyer };

Just like with arrays you can shallow clone an object using spreads:
const newObj = { ...oldObj };

Interfaces

TS uses “duck typing” also called “structural typing”. The idea is simple, as long as the object has the required elements, TypeScript doesn’t care what class/type/interface the object was defined with.

So if your function uses an object that has the property name: string it doesn’t matter if it’s a person, a building, a car, etc. Everything is acceptable as long as the object has a property name of type string.

Here are some examples:

Without interface

A lot of people miss the fact that you can specify object shapes as part of parameters (without resorting to classes or interfaces). This reduces the amount of boilerplate on your apps.

// Accept any object that has a property "label"
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
const myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

With interface

Of course nothing stops you from using an interface. Notice your object don’t have to “implement” the interface, just have the required shape.

interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

Optional properties

Sometimes you don’t need the full object to do a job.

interface Person {
name: string,
age?: number
}

In that case you can omit the property when you create the object:

const person: Person = { name: "Alan" };

Readonly properties

Continuing the immutable theme, readonly lets you prevent a property from being changed.

interface Person {
readonly name: string
}
const j: Person = { name: "John" };
j.name = "Paul"; // Error

Function types

TypeScript developers often overlook the fact that you can create a type/interface for a function. You can then use the type to declare variables or parameters of the function type.

// Print isn't the definition of a regular object, 
// it's the definition of a function!
interface Print {
(input: string): boolean;
}
const print: Print = (input) => true;
print("Hello"); // true

Indexable Types

When crafting the shape of the objects (types) you can also specify the kind of index the properties can be accessed by (string, number, enums, etc.)

enum Color {
red,
blue
}
type Switch = {
[key in Color]: boolean;
};
const options: Switch = { [Color.red]: true, [Color.blue]: false };

Functions

Optional and Default Parameters

Optional parameters are just a shorthand for | undefined. It means the parameter can be omitted, in which case it will have the value of undefined.

function test(
required: string,
optionalWithDefault: string = "A",
sameAsOptional: string | undefined,
optional?: string
) {
// required is guaranteed to have a value
console.log(required.length);
// optionalWithDefault is guaranteed to have a value
console.log(optionalWithDefault.length);
// Error: Object is possibly 'undefined'
console.log(optional.length);
}

Overloading

Avoid it. The nature of JS makes it very clunky to overload a function in TS.

Generics

Use and abuse generics. They work pretty much the same as in .Net land and other frameworks.

The following example creates a count function that takes an array of any type of elements.

function count<T>(arr: Array<T>): number {
return arr.length
}
count([1, 2, 3]);
count(["a", "b"]);

Enums

You can set and get specific values for enums:

enum Enum {
A,
B = "BBB",
C = 3
}
console.log(Enum.A); // 0
console.log(Enum[Enum.A]); // "A"
console.log(Enum.B); // "BBB"
console.log(Enum[Enum.B]); // Error: Enums have indexes of numbers
console.log(Enum.C); // 3
console.log(Enum[Enum.C]); // "C"

Enums can also be computed. This gives us a nice bitwise trick to create flags:

enum Enum {
A = 1,
B = 2 << 0,
C = 2 << 1,
D = 2 << 2,
E = 2 << 3,
}
Object
.keys(Enum)
.filter(k => !isNaN(+k))
.map(k => +k); // [1, 2, 4, 8, 16]

Type Inference

Use and abuse type inference. Let TS infer the types whenever possible. It makes code easier to maintain and refactor. In theory your app could be fully typed if you only type the parameters of your functions and the data coming from external sources, everything else should be able to be inferred by the compiler. That’s the theory, in practice you end up defining all kinds of types.

Intersection (&)

obj1 & obj2 essentially means all the properties of obj1 and all the properties of obj2.

interface Runner { run(): void; }
interface Swimmer { swim(): void; }
function doSomething(athlete: Runner & Swimmer) {
}
doSomething({ run() { }, swim() { } }); // Fine
doSomething({ run() { } }); // Error: Property 'swim' is missing in type '{ run(): void; }' but required in type 'Swimmer'.

Wait! What happens if the interfaces have a property with the same name but different signatures?

In that case you can’t intersect them.

Union (|)

The union type operator ( obj1 | obj2) is easier to understand, it means the object can be of either type. In this case it doesn’t matter if the interfaces have properties of the same name but different types. All’s goo as long as the object satisfies either interface.

interface Runner {
run(): void;
}
interface Swimmer {
swim(): void;
}
function doSomething(athlete: Runner | Swimmer) {
}
doSomething({ run() { }, swim() { } }); // Fine
doSomething({ run() { } }); // Fine

Most common use case:

function doSomething(value: string | number) {
}

Unions work with strings too. You can specify a list of strings allowed for the variable.

function doSomething(greeting: "Hello" | "Goodbye") {
}
doSomething("Hello"); // Fine
doSomething("Bye"); // Error: Argument of type '"Bye"' is not assignable to parameter of type '"Hello" | "Goodbye"'.

String|Numeric Literal Types

You can use union on pretty much any kind of value:

type Greeting = "Hello" | "Goodbye";
type Enthusiasm = 1 | 2 | 3;
function doSomething(greeting: Greeting, enthusiasm: Enthusiasm) {
}
doSomething("Hello", 1); // Fine
doSomething("Goodbye", 5); // Error: Argument of type '5' is not assignable to parameter of type 'Enthusiasm'.

Resources

TypeScript Handbook
TypeScript Playground
You Don’t Know JS
Mostly Adequate Guide to Functional Programming

--

--

Simon

Creator of SimonTest, a plugin that generates Angular tests.