TypeScript with NodeJS

Installing TypeScript

Choose one of these

yarn add --dev typescript

npm i typescript --save-dev

# globally
npm i -g typescript

This will allow you to run tsc command. To test whether a TypeScript file compiles you create one:

// /src/index.ts
const name = "Mathias";
console.log(name);

and then run tsc index.ts. You end up with an error:

.../node_modules/typescript/lib/lib.dom.d.ts:19620:15
    19620 declare const name: void;
                        ~~~~
    'name' was also declared here.

The reason is that name property is already defined on the window object (global scope), so you either have to change your variable name or preferably make your file a module by adding export{} to compile without errors:

// src/index.ts
const name = "Mathias";
console.log(name);
export {}

Let’s create another module other.ts and import it into index.ts to see how the compiler behaves (Will it compile all in one file or into two JS files?):

// index.ts
import sayHello from "./other";

const name = 'Mathias';
console.log(sayHello(name));

export {};
// other.ts
function sayHello(name: string) {
    console.log(`Hello ${name}`);
}

export default sayHello;

Executing tsc .\src\index.ts shows that TypeScript creates two js files, index.js and other.js.

Can I bundle my TS files into one JS file?

It is okay to compile many TS files into many JS files for a NodeJS project, but for a web front-end project we would rather like to have a single bundled file that we could load in the browser. Can that be done with TypeScript only? Yes, but with restrictions: You can use the compilerOption outFile to concatenate and emit output to a single file, but outFile cannot be used unless module is None, System, or AMD. That means, that this option cannot be used to bundle CommonJS or ES6 modules. For that to work, we need a bundler like Webpack.

How can I compile my TS files without specifying everything on the command line?

It is impractical to run tsc index.ts every time we change code. We could use TypeScript’s watch mode with tsc -w index.ts instead, but there is a better way. We define all our compile options once in a tsconfig.json and from now on simply run tsc or tsc -w without specifying any other parameters.

Configuring TypeScript (tsconfig.json)

Run tsc --init to create a tsconfig.json in your project root. Inspecting the file, you can see that there are many options, most of them commented out.

How should I adjust tsconfig.json?

  • You want all your TS files within src folder compiled? Add the include property
  • You want compilation output to a specific folder? Use outDir compiler option
  • You want to be able to debug your JS files? Use sourceMap: true
  • In some places, TypeScript doesn’t try to infer any types for us and instead falls back to the most lenient type called any. This defeats the purpose of using TypeScript in the first place, so you should set noImplicitAny: true.
  • strictNullChecks: true makes handling null and undefined more explicit, and spares us from worrying about whether we forgot to handle null and undefined

You could change them like this, to compile your TypeScript code to a dist folder, generate source maps and don’t allow implicit any types:

{
    "compilerOptions": {
        "module": "commonjs",
        "esModuleInterop": true,
        "target": "es6",
        "noImplicitAny": true,
        "moduleResolution": "node",
        "sourceMap": true,
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "*": [
                "node_modules/*",
                "src/types/*"
            ]
        }
    },
    "include": [
        "src/**/*"
    ]
}
// src/index.ts
const name = "Mathias";
console.log(name);
export {}

Now, when running tsc in your project root folder, your ts files under /src will be compiled into a /dist folder, including source maps. You can run tsc -w to watch for file changes and automatically compile changed files.

Basic and built-in Types

Boolean, Number, String, Array, Enum, Void, Null, Undefined, Never, Any.

Union Types

A Union Type is created by joining multiple types via the pipe symbol |, for example A | B.

A Union of Type A | B means…

  • the object (having this union type) must either be A or B
  • in other words: the object must have at least all members of A or all members of B
  • any other members that neither exist in A nor B will cause an error
type A = {
    age: number;
    name: string
}

type B = {
    skill: string;
    name: string;
}

const union: A | B = {
    age: 40,
    name: "test",
    skill: "",
}

// The following will cause an error, because the members are not enough to be either A or B:
// Property 'name' is missing in type '{ age: number; skill: string; }' but required in type 'B'.
const errorUnion: A | B = {
    age: 40,
    skill: "",
}

So if you have an object of type A | B you cannot really be sure whether you have an object that contains all members of A or all members of B. But you can be sure that the object has members that both A and B have. That’s why it is called Union type: you can be sure about the union of values both types have in common. To be sure about the actual type of an object, you must narrow down the type.

In the next example we have a union type number | string. Calling toString() on a type string | number works, because string and number both have implementations of toString(), but calling toUpperCase() gives an error:

function printId(id: number | string) {
  console.log(id.toUpperCase());
}
Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.

The solution is to narrow the union with code:

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

Intersection Types

Is mainly used to combine existing object types. Intersection type A & B means:

  • must have all members of A and B
  • must not have a member of another type
interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}

type ColorfulCircle = Colorful & Circle;

What is the difference between intersections and extending interfaces?

The principle difference between the two is how conflicts are handled, and that difference is typically one of the main reasons why you’d pick one over the other between an interface and a type alias of an intersection type.

Type Alias and Interface

Instead of defining the type inline like here

function printCoord(pt: { x: number;  y: number }) {

}

we can create a Type Alias which allows us to reuse the type again somewhere else:

type Point = {
  x: number;
  y: number;
};

function printCoord(pt: Point) {

}

We could also use an interface instead:

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

What are the differences between Type Aliases and Interfaces?

The key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable. Also, type aliases, unlike interfaces, can describe more than just object types, we can also use them to write other kinds of generic helper types:

type OrNull<Type> = Type | null;

type OneOrMany<Type> = Type | Type[];

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
           
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

Extending an interface works:

interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});

Extending a type does not work:

type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}

 // Error: Duplicate identifier 'Window'.

Extending types

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}

interface AddressWithUnit extends BasicAddress {
  unit: string;
}

interfaces can also extend from multiple types.

Type assertions

Sometimes you will have information about the type of a value that TypeScript can’t know about. In this situation, you can use a type assertion to specify a more specific type:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

// or (does not work in tsx files)
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

Literal types

In the following example we only allow “left”, “right” or “center” as second parameter to printText. We enforce that by defining literal types:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

In the following example req.method is inferred to be string, but the more specific type "GET" is expected. It is good that TypeScript considers this code to have an error because code between const req and handleRequest could have changed req.method to something like "GUESS".

const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);

Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

There are two ways to work around this.

Option 1: You can change the inference by adding a type assertion in either location:

// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");

Option 2: You can use as const to convert the entire object to be type literals

const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

Generic Types

TypeScript’s type system is very powerful because it allows expressing types in terms of other types. The simplest form of this idea is generics, but we actually have a wide variety of type operators available to us. It’s also possible to express types in terms of values that we already have.

By combining various type operators, we can express complex operations and values in a succinct, maintainable way.

function identity<Type>(arg: Type): Type {
  return arg;
}

// possible but not required to explicitly have <string> defined here
let output = identity<string>("myString");

// instead, TypeScript can infer the type itself
let output = identity("myString");

Instead of

interface NumberBox {
  contents: number;
}

interface StringBox {
  contents: string;
}

interface BooleanBox {
  contents: boolean;
}

better create a generic Box with a type parameter:

interface Box<Type> {
  contents: Type;
}

Then use it by specifying a type:

let box: Box<string>;

Think of Box as a template for a real type, where Type is a placeholder that will get replaced with some other type. When TypeScript sees Box<string>, it will replace every instance of Type in Box<Type> with string, and end up working with something like { contents: string }.

Having a generic type also means that we can avoid overloads entirely by instead using generic functions:

function setContents<Type>(box: Box<Type>, newContents: Type) {
  box.contents = newContents;
}

Array Type

// both type definitions are the same
const myArray: number[];
const myArray: Array<number>;

Modern JavaScript also provides other data structures which are generic, like Map<K, V>, Set<T>, and Promise<T>.

The ReadonlyArray is a special type that describes arrays that shouldn’t be changed.

// both type definitions are the same
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
const roArray: readonly string[] = ["red", "green", "blue"];

Non-null assertion operator

Only use ! when you know that the value can’t be null or undefined

function liveDangerously(x?: number | null) {
  // No error
  console.log(x!.toFixed());
}

Reference TS files

/// <reference path="person.ts" />

class Player implements Person {
  // ...
}

Module Resolution

You can import modules relatively

import {Something} from './Something';
import {Something} from '/Something';
import {Something} from '../bar/Something';

or Non-relatively:

import {Something} from 'mylibrary';

How does TypeScript find modules that you are referencing? Either via Classic or Node --moduleResolution strategy. Classic is the default when emitting AMD, System or ES2015 modules. Node is the default when emitting CommonJS or UMD modules.

Resolving Classic Relative Imports:

// assuming this file is /src/app/foo.ts
import {Something} from './Something';

// will look in this order:
/src/app/person.ts
/src/app/person.d.ts

Resolving Classic Non-relative Imports:

// assuming this file is /src/app/foo.ts
import {Something} from './Something';

// will look in this order:
/src/app/person.ts
/src/app/person.d.ts
/src/person.ts
/src/person.d.ts
// continue searching up the directory tree

Resolving Node Relative resolution:

// assuming this file is /src/app/foo.ts
import {Something} from 'person';

// will look in this order:
// /src/app/person.ts
// /src/app/person.tsx
// /src/app/person.d.ts
// /src/app/person/package.json (having a 'typings' property)
// /src/app/index.ts
// /src/app/index.tsx
// /src/app/index.d.ts

Resolving Node Non-relative resolution:

// assuming this file is /src/app/foo.ts
import {Something} from 'person';

// will look in this order:
// /src/app/node_modules/person.ts
// /src/app/node_modules/person.tsx
// /src/app/node_modules/person.d.ts
// /src/app/node_modules/person/package.json (having a 'typings' property)
// /src/app/node_modules/index.ts
// /src/app/node_modules/index.tsx
// /src/app/node_modules/index.d.ts
// now going up one directory but still looking in node_modules
// /src/node_modules/person.ts
// /src/node_modules/person.tsx
// /src/node_modules/person.d.ts
// /src/node_modules/person/package.json (having a 'typings' property)
// /src/node_modules/index.ts
// /src/node_modules/index.tsx
// /src/node_modules/index.d.ts
// continue searching up the directory tree

You can define a location that the compiler will look up when importing non-relative modules:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./modules"
  }
}

You can also specify specific lookup locations for specific modules:

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "my_lib" : ["./customPath"] // relative to baseUrl if specified
    }
  }
}

If you have source files located under several locations and you want to combine them, then you can use:

// tsconfig.json
{
  "compilerOptions": {
    "rootDirs": [
      "modules",
      "src",
      "ts/modules"
    ]
  }
}

To troubleshoot module resolution:

// tsconfig.json
{
  "compilerOptions": {
    "traceResolution": true
  }
}

Node with TypeScript

yarn add @types/node  

This Stack Overflow thread is about when to add @types to dependencies and when to devDependencies.

Type Guards

Type Guards are used to check whether you are dealing with a certain type or not. Getting from a more general type to a more specific type is called ‘Narrowing’. There are different ways to “type guard”, depending on what to test against. You can test against native values (string, number, boolean etc.), instances of classes or custom types.

Type Guard for native values

Here we check if year is a string or a number using typeof.

function showNextYear(year: string | number) {
    if(typeof year === "string") {
        const nextYear = parseInt(year, 10) + 1;
        console.log(`Next year is ${nextYear}`);
    } else {
        const nextYear = year + 1;
        console.log(`Next year is ${nextYear}`);
    }
}

showNextYear("2021");
showNextYear(2021);

Truthiness narrowing

Here we narrow down the type by checking the truthiness of strs:

function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

Instanceof Type Guard

class Deed {
    constructor(public action: string) { }
}

class Good extends Deed {
    constructor(action: string, public goodThing: string) {
        super(action);
    }
}

class Bad extends Deed {
    constructor(action: string, public badThing: string) {
        super(action);
    }
}

function yourAction(deed: Good | Bad) {
    if(deed instanceof Good) {
        console.log("Well done");
    } else {
        console.log("Please stop");
    }
}

yourAction({
    action: "donate",
    goodThing: "money"
});

yourAction({
    action: "waste",
    badThing: "food"
});

Custom Type Guard

interface Deed {
    action: string;
}

interface Good extends Deed {
    goodThing: string
}

interface Bad extends Deed {
    badThing: string
}

function isGood(deed: Deed): deed is Good {
    return deed.hasOwnProperty("goodThing");
}

function yourAction(deed: Good | Bad) {
    if(isGood(deed)) {
        console.log("Well done");
    } else {
        console.log("Please stop");
    }
}

yourAction({
    action: "donate",
    goodThing: "money"
});

yourAction({
    action: "waste",
    badThing: "food"
});

More narrowing

TypeScript is rather smart about automatically narrowing down types. In the following example we check if x is the same as y. If so, they both must be strings:

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.
    x.toUpperCase();
          
(method) String.toUpperCase(): string
    y.toLowerCase();
          
(method) String.toLowerCase(): string
  } else {
    console.log(x);
               
(parameter) x: string | number
    console.log(y);
               
(parameter) y: string | boolean
  }
}

Same goes for in operator:

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = {  swim?: () => void, fly?: () => void };

function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) { 
    animal
      
(parameter) animal: Fish | Human
  } else {
    animal
      
(parameter) animal: Bird | Human
  }
}

The never type

When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.

Some functions never return a value:

function fail(msg: string): never {
  throw new Error(msg);
}

The unknown type

The unknown type represents any value. This is similar to the any type, but is safer because it’s not legal to do anything with an unknown value:

function f1(a: any) {
  a.b(); // OK
}
function f2(a: unknown) {
  a.b();
Object is of type 'unknown'.

}

You can describe a function that returns a value of unknown type:

function safeParse(s: string): unknown {
  return JSON.parse(s);
}

// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);

The Function type

You should avoid untyped function calls like this, because of the unsafe any return type:

function doSomething(f: Function) {
  f(1, 2, 3);
}

If you need to accept an arbitrary function but don’t intend to call it, the type () => void is generally safer.

Basic Conditional Type

export interface ImageMeta {
  origin: string;
  format: "png" | "jpg";
}

export interface TextMeta {
  fontFoundry: string;
  licenseExpiration: Date;
}

function setMeta(layer: ImageLayer | TextLayer, meta: ImageMeta | TextMeta) {
    layer.meta = meta;
}

setMeta(textLayer, {
  format: "png",
  origin: ""
  // Problem: We passed ImageMeta to a TextLayer
  // but TypeScript does not complain, because setMeta function above allows this
});

We could solve this with function overloading:

function setMeta(layer: ImageLayer, meta: ImageMeta): void;
function setMeta(layer: TextLayer, meta: TextMeta): void;
function setMeta(layer: ImageLayer | TextLayer, meta: ImageMeta | TextMeta) {
    layer.meta = meta;
}

setMeta(textLayer, {
  format: "png",
  origin: ""
  // Good, now TypeScript complains that TextLayer was given ImageMeta
});
setMeta(textLayer, {
  fontFoundry: "something",
  licenseExpiration: new Date()
  // It works, but IntelliSense still offers ImageMeta (which we cannot use)
});

So the best solution is to use Conditional Types. First remove the overloaded functions, then add:

function setMeta<T extends TextLayer | ImageLayer>(
    layer: T,
    meta: T extends TextLayer ? TextMeta : ImageMeta) {
  layer.meta = meta;
}

setMeta(imageLayer, {
  format: "png",
  origin: ""
  // Works, and also great: IntelliSense only offers ImageMeta properties here
  // If the first param was textLayer, then IntelliSense offered TextMeta properties here
});

Function Type Expressions

// The syntax (a: string) => void means “a function with one parameter, named a, of type string, that doesn’t have a return value”
function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

function printToConsole(s: string) {
  console.log(s);
}

greeter(printToConsole);

Call Signatures

In JavaScript Functions can be callable and can additionally have properties:

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

Note that the syntax is slightly different compared to a function type expression

Construct Signatures

You can write a construct signature by adding the new keyword in front of a call signature:

type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

Index Signatures

Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values. An index signature property type must be either ‘string’ or ‘number’.

interface Dictionary<T> {
  [key: string]: T;
}

Conditional return types

function createLayer(type: LayerType): TextLayer | ImageLayer {
  if (type === LayerType.Text) {
    return {
      color: "#fff",
      fontSize: "10px",
      id: new Date().toISOString(),
      maxWidth: 10000,
      position: { x: 10, y: 10 },
      rotation: 0,
      text: "This is the default text",
      type: LayerType.Text
    } as TextLayer;
  }

  return {
    id: new Date().toISOString(),
    maxBounds: { width: 400 },
    position: { x: 0, y: 0 },
    rotation: 0,
    src: "ps-dark.png",
    type: LayerType.Image
  } as ImageLayer;
}

const textLayer = createLayer(LayerType.Text);
// Problem: textLayer can be TextLayer or ImageLayer which (if ImageLayer) does not have text property
textLayer.text = "can't set this";

To fix this, we have to adjust the return type of createLayer function:

function createLayer<T extends LayerType>(type: T): T extends LayerType.Text ? TextLayer : ImageLayer {
  if (type === LayerType.Text) {
    return {
      color: "#fff",
      fontSize: "10px",
      id: new Date().toISOString(),
      maxWidth: 10000,
      position: { x: 10, y: 10 },
      rotation: 0,
      text: "This is the default text",
      type: LayerType.Text
    } as T extends LayerType.Text ? TextLayer : ImageLayer;
  }

  return {
    id: new Date().toISOString(),
    maxBounds: { width: 400 },
    position: { x: 0, y: 0 },
    rotation: 0,
    src: "ps-dark.png",
    type: LayerType.Image
  } as T extends LayerType.Text ? TextLayer : ImageLayer;
}

const textLayer = createLayer(LayerType.Text);
textLayer.text = "now it can be set";

Now it works. To make it cleaner, you can refactor out the conditional return type to its own type, in this case FactoryLayer:

type FactoryLayer<T> = T extends LayerType.Text ? TextLayer : ImageLayer;

function createLayer<T extends LayerType>(type: T): FactoryLayer<T> {
  if (type === LayerType.Text) {
    return {
      // ...
    } as FactoryLayer<T>;
  }

  return {
    // ...
  } as FactoryLayer<T>;
}

Partial<T>

setTextLayerProps is a function that takes props and sets them on an object. Using Partial<T> (which is a build-in TypeScript feature) we can specify that only props of a specific type are allowed to be passed in as props. In the following example we only allow props from TextLayer.

function setTextLayerProps(project: Project, id: string, props: Partial<TextLayer>) {
  const layer = project.layers.find(l => l.id === id);

  if(layer) {
    Object.entries(props).forEach(([k, v]) => {
      (layer as any)[k] = v;
    });
  }
}

const project: Project = {
  layers: [imageLayer, textLayer],
  size: projectSize
};

setTextLayerProps(project, "10", {
  text: "hello",
  shouldNotWork: true // Good: TypeScript complains, that shouldNotWork is not part of the props of TextLayer
});

We can improve this example and make it more variable: We want to allow props depending on the generic type: If we define TextLayer, then only allow props from TextLayer, if we define ImageLayer, then only allow props from ImageLayer:

function setLayerProps<T extends TextLayer | ImageLayer>(project: Project, id: string, props: Partial<T>) {
  const layer = project.layers.find(l => l.id === id);

  if(layer) {
    Object.entries(props).forEach(([k, v]) => {
      (layer as any)[k] = v;
    });
  }
}

const project: Project = {
  layers: [imageLayer, textLayer],
  size: projectSize
};

setLayerProps<TextLayer>(project, "10", {
  text: "hello" // Good: only TextLayer props are allowed
});

setLayerProps<ImageLayer>(project, "20", {
  src: "some image source" // Good: only ImageLayer props are allowed
});

setLayerProps<ImageLayer>(project, "30", {
  src: "some image source",
  shouldNotWork: true  // Good, compiler complains
});

Lookup types

keyof

Lookup types, also referred to as indexed access types enable us to access the keys or the types for keys. Similar to how we can access the values of object properties, but for types.

type User = {
    id: number;
    name: string;
    place: string
}

type UserKeyTypes = User[keyof User];
// string | number

type UserKeys = keyof User;
// "id", "name", "place"

const works: UserKeys = "name";
const worksToo: UserKeys = "id";
const doesNotWork: UserKeys = 4711;

Mapping from one type to another

We want to ensure that you have all the properties of an existing type in another object:

const fieldDescriptions = {
  text: "This is the default text",
  anything: true // we don't want to allow this
};

Object.entries(fieldDescriptions).forEach(([field, description]) => {
  console.log(`${field}: ${description}`);
});

Solution 1:

type LayerCombined = TextLayer & ImageLayer;

type FieldDescriptions = {
  [key in keyof LayerCombined]: string;
}
const fieldDescriptions: FieldDescriptions = {
  text: "This is the default text",
  anything: true // Good, TypeScript complains that 'anything' does not exist in FieldDescriptions (that is: TextLayer)
  // Also good, TypeScript complains that other properties from FieldDescriptions (that is: TextLayer) are not specified here
};

Object.entries(fieldDescriptions).forEach(([field, description]) => {
  console.log(`${field}: ${description}`);
});

If you want to ensure all properties of two types, such as TextLayer and ImageLayer:

type FieldDescriptions = {
  [key in keyof (TextLayer & ImageLayer)]: string;
}

Only some of the properties of a type

Or in other words: All properties, except the ignored ones:

import { Project, TextLayer, ImageLayer, LayerType, Size } from "./types";

type LayerCombined = TextLayer & ImageLayer;
type IgnoredProperties = "id" | "maxBounds" | "position" | "meta";

type FieldDescriptions = {
  [key in Exclude<keyof LayerCombined, IgnoredProperties>]: string;
}

const fieldDescriptions: FieldDescriptions = {
  text: "This is the default text",
  // Good, typescript complains that 'anything' does not exist in TextLayer
};

Object.entries(fieldDescriptions).forEach(([field, description]) => {
  console.log(`${field}: ${description}`);
});

Mixins

If a class is getting too big and has too many responsibilities it should be split up. One way to do this is using Mixins. A Mixin is a function that returns a class. Let’s say you have a class ApiClient that has methods responsible for Projects and Images and you want to split out those methods to two mixins ProjectsAPI and ImagesAPI. But you still want to reference those methods through the class ApiClient:

# ProjectsAPI and ImagesAPI are functions (the Mixins) that each return a class
# Wrap them like this and you get both their functionalities
class ApiClient extends ProjectsAPI(ImagesAPI(CoreAPI)) {}

Now you can use methods related to Projects and Images in ApiClient. But the questions is of course, how do we code the Mixins themselves. This way:

export class CoreAPI {
  constructor(public baseUrl: string) {}
}

export type BaseAPIConstructor = new (...args: any[]) => CoreAPI;

function ImagesAPI<TBase extends BaseAPIConstructor>(Base: TBase) {
  return class extends Base {
    getImages(): Promise<{
      src: string;
      dimensions: Size;
    }> {
      return Promise.resolve({
        src: "http://a-fake-url.com/image-id",
        dimensions: {
          height: 100,
          width: 300
        }
      });
    }

    saveImage(src: string): Promise<number> {
      console.log(this.baseUrl);
      return Promise.resolve(10);
    }
  };
}

function ProjectsAPI<TBase extends BaseAPIConstructor>(Base: TBase) {
  return class extends Base {
    getProjects(): Promise<Project[]> {
      return Promise.resolve([]);
    }

    saveProject(): Promise<string> {
      return Promise.resolve("1");
    }

    deleteProject(): Promise<void> {
      return Promise.resolve();
    }
  };
}

Using Symbols

In the following example we have doLog, that checks if a log properties exists on the passed in object. If yes, then it assumes it is a function and executes it.

function doLog(message: string, obj: any) {
  const objStr = obj.log ? obj.log(obj) : obj.toString();
  console.log(`${message} ${objStr}`);
}

doLog("The first layer: ", layer);

That’s a bad, because we cannot guarantee that log is actually a logging function. The following example would break doLog:

const layer = {
  src: "dark.png",
  log: true  // this will break doLog
};

Another reasons why this is bad: If we iterated over the properties, log would show up, which is not what we want:

for (const key in layer) {
  if (layer.hasOwnProperty(key)) {
    const element = (layer as any)[key];
    console.log(`${key}:${element}`);
  }
}

We need to replace the property name log with something unique, that no object can accidentally overwrite: A Symbol. First, we want a function that adds a logging string to any object we pass in:

// assume you are in a logging library
const logHandler = Symbol();

function addLog<T>(obj: T, func: (obj: T) => string) {
  (obj as any)[logHandler] = func;
}

Now we add the logging functionality to an object:

// in the package consumer
const layer = {
  src: "dark.png",
  log: true
};

addLog(layer, (obj: { src: string }) => `An image layer with src: ${obj.src}`);

Finally, when we run doLog we can be sure that log is a unique value, because we are using a Symbol.

function doLog(message: string, obj: any) {
  const objStr = obj[logHandler] ? obj[logHandler](obj) : obj.toString();
  console.log(`${message} ${objStr}`);
}

doLog("The first layer: ", layer);

Async TypeScript Generator

type Person = {
    name: string;
    title: string;
}

let items: Person[] = [
    {
        name: "Bing Bong",
        title: "The Archer"
    },
    {
        name: "Hector",
        title: "The well endowed"
    },
    {
        name: "Brutalitops",
        title: "The Magician"
    }
];

async function* getItems(): AsyncGenerator<Person> {
    for (let item of items) {
        await new Promise(r => setTimeout(r, 4000));
        yield item;
    }
}

async function renderOverTime() {
    for await (let item of getItems()) {
        console.log(`${item.name} ${new Date().toLocaleTimeString()}`);
    }
}

renderOverTime();

Tuples

Tuples enable you to describe what type will be at what index:

type NumNumStrTuple = [number, number, string];

const myTuple: NumNumStrTuple = [4, 1, "hey"];
const myBadTuple: NumNumStrTuple = ["4", 1, "hey"]; // Error: "4" is not a number
const myOtherBadTuple: NumNumStrTuple = [4, 1]; // Error: missing string on third position

Omit property from type

Omit allows you to create a new type based on an existing type, but specify properties of the existing type that you want to have omitted in the new type (don’t mix that up with Exclude):

Omit<{ a: string, b:string }, 'a' > === { b: string }

Omit is the opposite of Pick.

Pick<{ a: string, b:string }, 'a' > === { a: string } 

Exclude is different from Omit, because it takes a union type and removes a constituent of that union.

Exclude<string | number, string > === number

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.