The Pragmatic TypeScript Programmer

Get the most out of TypeScript with simple, pragmatic and functional best practices.

by Gion Kunz

Gion Kunz

  • Angular GDE since 2018
  • Front-end developer, web standards advocate
  • Specialized in spa development using Angular
  • Onsite ramp-ups, coaching and implementation support in projects
  • Author of the book "Mastering Angular Components"
  • Frequent speaker at conferences and meetups
  • Teacher for web development and UX engineering at the SAE Institute in Zurich
  • Tutor at O'Reilly Safari Online Training
  • Contributor on the Angular project, author of Chartist.js library

Founder, UI Developer, UX Enthusiast at syncrea GmbH

If I am not in the Web universe, I watching Deep Space.

M82 (Bode's Galaxy, left) and M81 (Cigar Galaxy, right), both about 12 Mio Lightyears away.

Workshop Topics

  • TypeScript Base

    • No Implicit Any

    • Any and Unknown

    • Strict null checks

  • Type Declarations

    • Use native types

    • Declaration by initialization

    • Tuple type declarations

    • Literal types

    • Union types (and & or)

    • String literal union types

  • ECMAScript 2015 - 2018

    • Modules

    • Var vs. Let vs. Const

    • Template Strings

    • Loops (for, for of, forEach)

    • Arrow Functions

    • Destructuring

    • Spreading

    • async / await

  • Functional Primer

    • Pure functions

    • Immutability

  • OOP > FP > Pragmatic TypeScript

    • Object literals with Interfaces

    • The Myth of OOP

  • Advanced Topics

    • Type aliases for Documentation

    • Discriminated Union Types

    • The Partial Interface

    • The keyof Type

    • Conditional Generic Types

    • User Defined Type Guards with type predicates

Prerequisites

  1. You have your Laptop with internet access
  2. Access to https://stackblitz.com
  3. If you want to keep your history, create an account!

Let's Hack!

TypeScript Base

No Implicit Any

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

Any and Unknown

let x: any = 10;
x = 'anything';

let y: unknown = 5;
y = 'unknown';

let z: number;
z = x;
// Not possible!
z = y;

Strict Null-Checks

// tsconfig.json
{
  "compilerOptions": {
     "noImplicitAny": true,
     "strictNullChecks": true
  }
}

How to Deal with possible null values

  • Non-Null Assertion Operator !
  • die() helper function
let x: string | null;

x!.toLowerCase();

function die(message: string): never {
  throw new Error(message);
}

(x || die('Ooops...')).toLowerCase();

Type Declarations

Use "Native" Types

let n: number;
// Not this -> let n: Number;

let b: boolean;
// Not this -> let b: Boolean;

let s: string;
// Not this -> let s: String;

Skip Declaration when Initialized with Value

const n = 100;
// Not this -> const n: number = 100;

const x = 'Hello World';
// Not this -> const x: string = 'Hello World';

Tuple Type Declarations

type NameAgeTuple = [string, number];

const nameAndAge: NameAgeTuple = ['Peter', 52];

Literal Types

type AnswerToAllQuestions = 42;
// Error!
const n: AnswerToAllQuestions = 13;

Union Literal Types

type FibonacciNumber = 1 | 2 | 3 | 5 | 8 | 13;
// Error!
const n: FibonacciNumber = 4;

String Literal Union Types as Enumeration Type

type OrderStatus = 'Ordered' | 'Paid' | 'Shipped' | 'Delivered';

interface Order {
  id: number;
  status: OrderStatus;
}

const order: Order = {
  id: 1,
  status: 'Ordered'
};

Intersection Types

interface Cat {
  name: string;
  meow: () => void;
}

interface Dog {
  name: string;
  bark: () => void;
}

export type CatDog = Cat & Dog;

ECMAScript 2015 - 2019

ECMAScript Modules

// a.ts
const magicNumber = 13;

export const modulePublic = 'This is public';

export function multiplyWithMagicNumber(n: number): number {
  return magicNumber * n;
}

// Default export
export default [1, 2, 3, 5, 8, 13];
// index.ts
import {
  modulePublic as moduleAText, 
  multiplyWithMagicNumber
} from './a';
import fibonacci from './a';
import * as A from './a';

console.log(moduleAText);
console.log(multiplyWithMagicNumber(7));
console.log(fibonacci);

console.log(A.default === fibonacci);
console.log(A.modulePublic === moduleAText);

Var vs. Let vs. Const

function closure() {
  var closureScoped = 1;
  for(let i = 0; i < 10; i++) {

  }
  // Error
  console.log(i);
}

Template Strings

const peter = {
  name: 'Peter'
};

console.log(`My name is ${peter.name}!`);

Loops Revived

const list = [1, 2, 3, 4, 5, 6, 7];

// Traditional but block scope counter
for(let i = 0; i < list.length; i++) {
  console.log(i);
}

// Functional
list.forEach(console.log);

// for...of loop
for(let item of list) {
  console.log(item);
}

Arrow Functions

const multiply = (a: number, b: number) => a * b;

Destructuring

// Array destructuring
const list = [1, 2, 3];
const [first, second, third] = list;

// Object destructuring
const peter = {
  firstName: 'Peter',
  lastName: 'Griffin'
};
const {firstName, lastName} = peter;

Parameter Destructuring and Defaults

export interface ConfigurationOptions {
  delay: number;
  host: string;
  limit: number;
}

function configure({delay = 1000, 
                    host = 'localhost',
                    limit = 300}: Partial<ConfigurationOptions>) {
  console.log(host);
  console.log(delay);
  console.log(limit);
}

Spreading

// Array spreading
const list = [1, 2, 3, 4];
const appended = [...list, 5, 6, 7];

// Object property spreading
const peter = {
  firstName: 'Peter',
  lastName: 'Griffin'
};
const peterWithAge = {
  ...peter,
  age: 52
};

Async / Await

// Traditional promise chain
function invalidateUsersLame() {
  return fetch('/api/users')
    .then(usersResponse => usersResponse.json())
    .then((users: User[]) => 
      Promise.all(users.map(user => 
        fetch(`/api/users/${user.id}/invalidate`).then(() => user)
      ))
    );
}

Async / Await

// Async await löve!
async function invalidateUsers() {
  const usersResponse = await fetch('/api/users');
  const users: User[] = await usersResponse.json();
  for(let user of users) {
    await fetch(`/api/users/${user.id}/invalidate`);
  }
  return users;
}

Functional Primer

Pure Functions

// Pure
function multiply(a: number, b: number): number {
  return a * b;
}

// Impure
const name = 'Peter';

function getNameLength() {
  return name.length;
}

As Much Pureness as Possible!

  • Very simple to reason about
  • No dependencies other than parameter
  • Reproducable
  • Highly deterministic
  • Simple to test
  • Cachable!

Immutability - What's wrong with mutability?

  • Object property mutation changes state, but consumers are not notified.
  • Mutable data is inherently complex, because it can change.
  • Mutable data can lead to distributed dependencies, which would need to be notified about change.
  • Harder to be confident about correctness of the code.
  • Reference Problem: The nested references

Immutability by using readonly

interface Person {
  readonly name: string;
  readonly age: number;
}

const person: Person = {
  name: 'Peter',
  age: 52
};

// Error!
person.name = 'Pete';

Convert mutable structures with Readonly<T>

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

const person: Readonly<Person> = {
  name: 'Peter',
  age: 52
};

// Error!
person.name = 'Pete';

Immutable Arrays, Sets and Maps

const list: ReadonlyArray<number> = [1, 2, 3, 4];
// Error!
list[0] = 10;
// Error!
list.sort();
// Error!
list.fill(42);

OOP > FP > Pragmatic TypeScript

Interfaces with Object Literals

export interface Person {
  firstName: string;
  lastName: string;
}

const person: Person = {
  firstName: 'Peter',
  lastName: 'Griffin'
};

Interfaces with Object Literals

  • Use interfaces to define your model / data.
  • If too much boilerplate for construction, use object factory functions.
  • Implement any logic outside your model using pure helper functions.

What's Wrong with OOP and Encapsulation?

  • One does not combine data and behavior to create living objects...
  • Mostly the types with behaviors metaphor is not taking you that far.
  • We have learned that Inheritance is a bad thing to begin with and we should avoid it.
  • Implementing behavior classes and using composition is suggested but it's just a work-around.
  • Encapsulation ≠ Data Hiding

The Multiple Inheritance Dilemma

export class DesigningDeveloper {
  design(): void {
    // Can design
  }

  develop(): void {
    // Can develop
  }
}

Advanced Topics

Type aliases for Documentation

export type PersonId = number;

function getPerson(id: PersonId) {

}

Discriminated Union Types

export interface Apple {
  readonly kind: 'Apple';
  readonly color: string;
}

export interface Banana {
  readonly kind: 'Banana';
  readonly bendRadius: number;
}

export type Fruit = Apple | Banana;

The Partial Interface

export interface Person {
  readonly name: string;
  readonly age: number;
  readonly isHungry: boolean;
}

function updatePerson(data: Partial<Person>) {
  // Do stuff with partial person data...
}

The keyof Type

export interface Person {
  readonly name: string;
  readonly age: number;
  readonly isHungry: boolean;
}

export type PersonKey = keyof Person;

Combine keyof with Generics

// First version... We can do better!
function getPropertyLame<T>(object: T, key: keyof T) {
  return object[key];
}

// That's fantastic!
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
  return object[key];
}

Mapped Types with keyof

export type Partial<T> = {
  [K in keyof T]?: T[K]; 
};

Conditional Generic Types

export type YesNo<T extends boolean> = 
  T extends true ? 'Yes' : 'No';

Infer Types in Conditional Generics

export type Unpacked<T> = 
  T extends (infer A)[] ? A : 
  T extends Promise<infer P> ? P : 
  T extends Set<infer S> ? S :
  T extends Map<infer MK, infer MV> ? [MK, MV] :
  T;

Let's create together.

Thanks!

Public: Pragmatic TypeScript Programmer

By Gion Kunz

Public: Pragmatic TypeScript Programmer

Slides for our Pragmatic TypeScript Workshop

  • 652