← Home

TypeScript

Setup

Installing

To install typescript globally:

npm i typescript -g

To install typescript locally:

npm i typescript --save-dev

Compiling

To compile a .ts file into a .js file:

tsc script.ts

To initialize a TS project by creating a tsconfig.json:

tsc --init

To compile all the files in the project:

tsc

To compile all .ts files and continuously watch for edits:

tsc -w

To configure the compiler:

tsconfig.json
{
  "rootDir": "./src",
  "outDir": "./build"
}

The flag noImplicitAny instructs the compiler to raise an error when it cannot infer the type of a variable and, therefore, when it would be forced to cast it as an implicit any type.

Defining

Type definition files

A type definition file, with the .d.ts extension, is a file containing custom type declarations, usually for a third-party library that does not provide types.

The point of the type definition file is to define the types of variables, objects, functions, etc. in some code. In a .d.ts file, each root-level definition must start with the declare keyword.

Some third-party libraries come with type definition files included when installed via npm i package. Other third-party libraries do not provide type definition files, usually causing the error: Could not find a declaration file for module 'package'.

To manually install types for a package:

npm i @types/package --save-dev

@types definition files are downloaded from Definitely Typed.

Node.js does not come with its types included. You need to manualy install them. The package name is node.

Declaration merging

The compiler merges two separate declarations having the same name into a single definition.

An interface with some properties and methods will be combined with another interface having other properties and methods, but having the same name, into a single interface with that name and with all the properties and methods.

This is useful for module augmentation, i.e. extending a module with new members when it is a third-party module.

Serving

Localhost. TypeScript is often served on localhost with parcel-bundler. Place a link to index.ts in your index.html file. Then, to serve the html via the following to autocompile index.ts into index.js.

parcel index.html

Node. TypeScript can also be served using nodemon and concurrently:

package.json
"scripts": {
    "start:build": "tsc -w", // re-compile
    "start:run": "nodemon build/index.js", // re-serve
    "start": "concurrently npm:start:*"
},

Typing

Typing, a.k.a. type annotation, means assigning a type to a variable, parameter, return value, etc. For typing an element, use a colon : and the type. Anything after the colon but before the equal sign is a typing or type annotation.

const apples: number = 5;

Type classification

  • primitive types

    • string
    • number
    • boolean
    • any / undefined
    • null
  • object types

    • typed array
    • bidimensional array
    • union-type array
    • tuple
    • object literal
    • class
    • interfaced class
    • function skeleton
    • enum
  • special types

    • void
    • never
  • union type
  • intersection type

Primitive types

Type string

const myString: string = "hello";

Type number

const myNumber: number = 0.1;

Type boolean

const myBoolean: boolean = true;

Type any / undefined

let whatever; // inferred `any` -- avoid!
let whatever: undefined; // -- avoid!

The variable is declared but not initialized, i.e. it holds no value. Do not confuse type undefined with value undefined!

Type null

let nothing: null = null;

The variable is declared and initialized with a value that represents emptiness (the intentional absence of any value). Do not confuse type null with value null!

Object types

Unlike primitive types, object types are created with a constructor function.

  • typed array

    • bidimensional array
    • union-type array
    • tuple
  • object literal
  • class

    • interfaced class
  • function skeleton
  • enum

Type: typed array

An array only allowing for a given type.

let colors: string[] = ["red", "blue"];

There are three subcategories of typed arrays:

  • bidimensional array
  • union-type array
  • tuple

Subtype: Bidimensional array

let colors: string[][] = [
  ["red", "blue", "yellow"],
  ["brown", "grey", "purple"],
]; // an array of arrays of strings

Subtype: Union-type array

let manyTypeArray = ["hi", 20]; // inferred: (string | number)[]

let singleTypeArray = ["hi"]; // inferred: string[]
singleTypeArray.push(20); // error

The type of the array is inferred at initialization, so if you initialize a single-type array and then try to add a variable with a different type, TypeScript will error, unless you specify the union type at initialization.

let singleTypeArray: string | number = ["hi"];
singleTypeArray.push(20); // okay

Subtype: Tuple

Union-type arrays disregard order. To enforce the order of types inside an array, instead of using a union-type array, list the types one after another to create a typed tuple:

let pepsi: [string, boolean, number] = ["brown", true, 40];

A typed tuple is often extracted out into a type alias:

type Drink = [string, boolean, number];
let pepsi: Drink = ["brown", true, 40];

Type: object literal

let point: { x: number; y: number } = {
  x: 10,
  y: 20,
};

Type: class

class Car {
  // ...
}

let myChevy: Car = new Car();

Subtype: Interfaced class

const myCollection: Collection<Sortable> = new Collection<Sortable>();

Type: function skeleton

let logNumber: (n: number) => void = (n) => {
  console.log(n);
};

The type here is the pairing of argument with type number and return value with type void.

Type: enum

function printResult(result: MatchResult) {
  // ...
}

An enum (enumeration) is an object that stores a small collection of closely related values known at compile time. The point of an enum is only to signal to other developers that this is a small collection of closely related values known at compile time.

enum MatchResult {
  HomeWin = "H",
  AwayWin = "A",
  Draw = "D",
}

An enum is a type.

function printResult(result: MatchResult) {
  // ...
}

When compiled into .js, an enum becomes an object.

It is possible to define an enum with only keys and no values:

enum MatchResult {
  HomeWin,
  AwayWin,
  Draw,
}

Special types

Type: void

function sayHello(): void {
  console.log("Hello");
}
// no return value

Type: never

function errorThrower(msg): never {
  throw Error(msg);
}
// function never returns, so
// it can never be completely executed

Union type

The | operator specifies any of two or more types:

let collection: number[] | string;

Accessible members for collection are only those in common for the types number[] and string. For example, push() is discarded because it does not exist for string, but indexing is allowed for reading number[] and string.

Union means OR, union means that something is either a number or a string.

You can use a union type in a type alias:

type alphanum = string | number;

You can use the union type with string literals in order to enforce that only specific strings be allowed as options. (String literals are thus treated as distinct types.)

type CardinalDirection = "North" | "East" | "South" | "West";

function move(steps: number, direction: CardinalDirection) {
  // ...
}

move(1, "North"); // okay
move(1, "Nurth"); // error

The above also works for numbers:

type OneToFive = 1 | 2 | 3 | 4 | 5;

myNumber: OneToFive = 5; // okay
myNumber: OneToFive = 6; // error

You can also use string literals together with type aliases:

type EmployeeCategory = "Manager" | "Non-Manager";

Intersection type

The & operator specifies all of two or more types:

let phablet: Phone & Tablet;
// `phablet` will have all of the members of `Phone` and `Tablet`

Intersection means AND, the object has to satisfy both InterfaceA and InterfaceB.

Type inference

A variable declaration is let or const plus a variable name.

let apples;

A variable initialization is variable name with = plus value.

apples = 2;

If declaration and initialization occur on the same line, TypeScript will infer the type of the variable from its contents.

let apples = 2; // inferred: `number`

If declaration and initialization occur on different lines, TypeScript will not infer the type of the variable. TypeScript will simply assign any to it.

let apples; // uninitialized, no inference, assigned: `any`
apples = 5;

No inference cases

There are four cases where there is no type inference, so you must use type annotations:

1. Variable with separate declaration and intitialization

let words = ["red", "green", "blue"];
let foundWord; // assigned: `any`

for (let i = 0; i > words.length; i++) {
    if (words[i]) === "green" {
        foundWord = true;
    }
}

To fix, annotate with a type:

let foundWord: boolean;

2. Pre-coded function returning any

let jsonString = "{'x': 10, 'y': 20}";
let result = JSON.parse(jsonString);
// return value of JSON.parse() is type `any`
// `result` is assigned type `any`

To fix, annotate with a type:

let result: { x: number; y: number } = JSON.parse(jsonString);

3. All function arguments and return values

A function’s return value can be inferred, but always specify the return value type anyway.

Omitting the return value type leads to an inference of void. If you type the return value and then, by mistake, you forget to include the return keyword, TypeScript will detect the mistake.

const add = (x: number, y: number) => {
  a + b;
};
// no typed return value, forgot `return` and TypeScript does not realize
const add = (x: number, y: number): number => {
  a + b;
};
// typed return value, forgot `return` but TypeScript complains!

4. Variable accepting two or more types

let numbers = [-10, -1, 12];
let numberAboveZero = false;
// inferred: `boolean`, but code logic allows for `number` as well

for (let i = 0; i < numbers.length; i++) {
    if (numbers[i]) > 0 {
        numberAboveZero = numbers[i];
    }
}

To fix, annotate with a union type:

let numberAboveZero: boolean | number = false;

5. Empty array

const myArray = []; // assigned: `any`

To fix, annotate with a type:

const myArray: string[] = [];

Typing entities

Typing return values

If the types of the arguments in a function are known, the return value will also be inferred.

let sumNumbers(x: number, y: number) {
    return x + y; // return value inferred: `number`
};

Typing functions

You can type the function arguments and return value:

function Add(x: number, y: number): number {
  return x + y;
}

And, instead of typing the arguments and return value separately, you can also type the function skeleton itself:

type AddStructure = (x: number, y: number) => number;

let add: AddStructure = (x: number, y: number): number => x + y;

Typing in destructuring

To destructure (pick) a property from an object and annotate the type at the same time, you declare the variable between curly brackets and annotate the property with a type, surrounding both the property and its type between curly brackets as well.

let john = {
  name: "john",
  age: 20,
};

const { age }: { age: number } = john; // typed destructuring
console.log(age); // → `20`

const {
  name,
  age,
}: {
  // typed destructuring
  name: string;
  age: number;
} = john;
console.log(name); // → `john`
console.log(age); // → `20`

Destructuring in an argument:

let myForecast = {
  date: new Date(),
  weather: "sunny",
};

Without destructuring:

const logWeather = (forecast: { date: Date; weather: string }) => {
  console.log(forecast.date);
  console.log(forecast.weather);
};

With destructuring:

const logWeather = ({ date, weather }: { date: Date; weather: string }) => {
  console.log(date);
  console.log(weather);
};

Type aliases

A type alias is custom type intended for reuse. An assignment with = is required.

type StrOrNum = string | number;

let sample: StrOrNum;

When used for a function, a type alias declares the type of a function without implementing it:

type LongHand = {
    (a: number): number
};

type ShortHand = (a: number): number;

It allows for readonly:

type Foo = {
  readonly bar: number;
  readonly bas: number;
};

It is often used for void callbacks:

type VoidCallback = () => void;

Type assertions

A type assertion overrides a type inference. This is useful when porting code over from JavaScript.

var foo = {};
foo.bar = 123; // Error: property 'bar' does not exist on `{}`
foo.bas = "hello"; // Error: property 'bas' does not exist on `{}`

This errors by default because the type of foo is inferred to be an empty object, i.e., an object without properties.

Interfaces are open-ended, but type inferences are strict.

To override the type inference, use the keyword as plus an interface:

interface Foo {
  bar: number;
  bas: string;
}
var foo = {} as Foo; // type assertion
foo.bar = 123;
foo.bas = "hello";

The above is identical to:

var foo: Foo = {};

Interfaces

An interface is a type that describes the shape of an object. Usually intended for reuse. The shape is the minimum required properties and types of the object. Interfaces are open-ended, so its properties are required, but more properties can be added. No assignment = after the name. Mind the semicolons ; between properties, just as with object literals.

We tend to prefer to interfaces over types aliases to define the shapes of objects because interfaces are more powerful. Interfaces offer the same functionality when it comes to using them as type annotations, but interfaces offer more things around using them to extend various things and to implement a particular class.

interface User {
  name: string;
  age: number;
}

let john = {
  name: "john",
  age: 18,
};

function printUserName(user: User) {
  console.log(user.name);
}

printUserName(john);

An interface can also contain a function:

interface Vehicle {
  name: string;
  year: Date;
  broken: boolean;
  summary(): string;
  // to satisfy this interface, the object
  // must have a function called `summary()`
  // that returns a `string`
}

To make an interface property optional, use ? after the property (otherwise, just delete the property):

interface SquareConfig {
  color?: string;
  width?: number;
}

To make an interface property read-only (i.e., modifiable only when an object is first created), use readonly before the property:

interface Point {
  readonly x: number;
  readonly y: number;
}

Create functions that accept arguments typed with interfaces. The interface should serve as the “gatekeeper” for the function!

Interface extension

An interface can extend another interface with the extends keyword, i.e., an interface can copy the properties of another and add to (or override) them.

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

Interface implementation

With the implements keyword, a class must satisfy an interface, i.e. the implementing class is required to have, as its properties and methods, all the properties and functions in the interface. This way, the class members become dependent on the interface.

interface Point {
  x: number;
  y: number;
}

// class MUST satisfy interface
class ColorPoint implements Point {
  x: number; // omitting this → error
  y: number; // omitting this → error
  color: string;
}

This is optional, but helps TypeScript pinpoint where an error is.

Function interfaces

// create a function interface
interface DoubleValueFunc {
  (number1: number, number2: number): number;
  // two arguments of type `number`, return value of type `number`
}

// use the function interface when initializing a variable
let myDoubleFunction: DoubleValueFunc;

// assign a function to the variable having the interface
myDoubleFunction = function(value1: number, value2: number) {
  return (value1 + value2) * 2;
};

Classes

Ordinary classes

TypeScript classes double up as (A) encapsulators of variables and functions, and (B) as types.

Class property declaration

ES6 class, for reference:

class MyClass {
  constructor() {
    this.myclassvar = "abc";
  }
}

TypeScript requires a class property to be declared before the constructor:

class MyClass {
  myclassvar: string;
  constructor() {
    this.myclassvar = "abc";
  }
}

Class constructor shorthand

With constructor for accepting a parameter:

class Vehicle {
  color: string;
  constructor(color: string) {
    this.color = color;
  }
}

let myCar = new Vehicle("orange car");

Constructor shorthand for the above:

class Vehicle {
  constructor(public color: string) {}
  // `public` takes argument and directly assigns it to class property
  // no property and no constructor body needed
}

let myCar = new Vehicle("orange car");

Instead of public, the other modifiers private and protected can also be used, with their respective effects.

When defining a constructor for a child class, the constructor in the child class must contain super(), which calls the parent’s constructor.

class Vehicle {
  constructor(public color: string) {}
}

class Car extends Vehicle {
  constructor(public wheels: number, color: string) {
    super(color); // calls parent's constructor with second argument
  }
}

let myCar = new Car(4, "red");

Class property modifiers

public

A member is public by default: it can be freely accessed from outside its containing class.

class Animal {
  name: string; // public by default
  constructor(name: string) {
    this.name = name;
  } // ok, access to public property inside class
}

let cat = new Animal();
console.log(cat.name); // ok, access to public property outside class

private

If a member is marked private, it can only be called by other members in this class.

class Animal {
  private name: string;
  constructor(name: string) {
    this.name = name;
  } // ok, access to private property inside class
}

let cat = new Animal();
console.log(cat.name); // error, access to private property from outside class

When overriding a method in a child class, the modifier must be maintained. If a method in the parent is public, the child cannot privatize it.

The purpose of privatizing a class member is to restrict access to it, usually by another developer, to prevent errors. A private property cannot be referenced and a private method cannot be called, unless the member in the class module is edited.

protected

If a member is marked protected, it can only be accessed by other members in this class and by members in child classes of this class.

class Person {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Employee extends Person {
  private department: string;

  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }

  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  } // ok, access to protected property belonging to parent
}

let howard = new Employee("Howard", "Sales");
console.log(howard.name); // error, access to protected property from outside class

If a member is marked readonly, it cannot be modified. Readonly properties must be initialized at their declaration or in the constructor.

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor(theName: string) {
    this.name = theName;
  }
}

let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is read-only

If a member is marked static, it exists on the class itself rather than on the instances.

class ClassName {
  static staticProperty = { x: 0, y: 0 };
}

console.log(myClassName.staticProperty);
// no need to instantiate the class

Abstract classes

Abstract classes are base classes from which other classes are derived. Abstract classes are not be instantiated directly, are only used as parent classes, can have real implementation of methods, can have implemented methods that do not exist yet (as long as they have names and types), and can make child classes promise to implement methods.

abstract class ClassName() {
    abstract method(param): type;
    // child will implement abstract method
    abstract property: type;
    // child will implement abstract property
}

Also, methods in an abstract class can be marked as abstract. Abstract methods do not contain an implementation and must be implemented in derived classes.

abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log("roaming the earth...");
  }
}

Abstract class (inheritance) vs. interface (composition)

An abstract class sets up a contract between rather similar classes, allowing them to work together with tight coupling. Example: An abstract class CSVReader and its child class MatchReader. A different child satisfying the abstract class can be instantiated, with custom methods for extracting match data, based on the data source: a CSV file, an API, etc. Inheritance: A MatchReader is a subclass of CSVReader.

An interface sets up a contract between very different classes, allowing them to work together with loose coupling. Example: A class MatchReader taking in an argument of reader with an interface having a read() method and a data property, the argument being CSVReader. A different reader satisfying that interface can be passed in, based on the data source: a CSV file, an API, etc. Composition: A MatchReader has a reference (in its constructor argument) to CSVReader.

Design patterns

Interfaces as class gatekeepers

Use interfaces as gatekeepers for class constructor parameters and class method parameters. Interfaces should define how an argument be eligible to be used by a class method. Interfaces are sometimes named with the -able suffix like Sortable, i.e., if the entity satisfies the interface for being sorted, it can be passed to a class method. The point is that the entity being passed in can be swapped without needing to change the interfaced class, only its instantiation.

// class method
addMarker(mappable: User | Company): void {
    new google.maps.Marker({
        map: this.googleMap,
        position: {
            lat: mappable.location.lat,
            lng: mappable.location.lat
        }
    })
}

This is not future-proof because of high coupling. To fix this, annotate a class method parameter with an interface, not a class. This keeps the parameter flexible for any future classes. The class must satisfy the interface in order to be accepted by the class method.

interface Mappable {
    location: {
        lat: number,
        lng: number
    }
}

// class method
addMarker(mappable: Mappable): void {
    // ...
}

In short, interfaces are contracts for classes to operate with each other.

TypeScript + Express

Install Express and its types:

npm i express
npm i express @types/express --save-dev

Startup:

import express, { Request, Response } from "express";

const app = express();

app.get("/", (req: Request, res: Response) => {
  res.send("hello");
});

app.listen(3000, () => {
  console.log("Listening on port 3000");
});

Interface for request with body:

import { Request } from "express";

interface RequestWithBody extends Request {
  body: { [key: string]: string | undefined };
} // `undefined` for empty request body

Router:

import { Router, Request, Response } from "express";

const router = Router();

router.post("/login", (req: RequestWithBody, res: Response) => {
  res.send("hello at login");
});

TypeScript + React

Typing props

To pass props to a class-based component:

interface AppProps {
  color: string;
}

class App extends React.Component<AppProps> {
  // ...
}

To pass props to a stateless functional component:

const App = (props: AppProps): JSX.Element => {
  // ...
};

Typing state

In a class-based component, state can be initialized by satisfying an interface:

interface AppProps {
  color: string;
}

interface AppState {
  counter: number;
}

class App extends React.Component<AppProps, AppState> {
  constructor(props: AppProps) {
    super(props);
    this.state = { counter: 0 };
  }
}

Or in the absence of an interface:

interface AppProps {
  color: string;
}

class App extends React.Component<AppProps> {
  state = { counter: 0 };
}

Do not mix state initialization modes.