Check your domain

Check your domain

Domain driven development with TypeScript

Overview

  • TypeScript
  • The Domain
  • Some Problems
  • Some tools
  • Why do this?

What is TypeScript?

  • Statically typed programming language
  • Structural typing system
  • OOP and FP
  • Compiled to Javascript
  • Not "Javascript with Types"

The Domain

  • Manufacturing of Planks
  • Sustainable sourcing initiative
  • After a plank is cut to size it must pass QA before being shipped

Code: Defining the Model

ddd-with-ts/01-domain.ts
export type Plank = {
  material: string

  serialNumber: string
  manufacturedDate: Date

  passedQA: boolean

  shipped: boolean
  shippingDate: Date

  height: number
  width: number
}

Poke some holes

Code: What potential issues are there in our model

ddd-with-ts/02-domain-issues.ts
export type Plank = {
  /** Is this just a string? */
  material: string

  /** Is this some random bit of text? Does it have a structure */
  serialNumber: string

  /** Are there any constraints on this Date? */
  manufacturedDate: Date

  passedQA: boolean

  /** Could we ship something that did not pass QA */
  shipped: boolean
  shippingDate: Date

  /** What units are these measured in? How do we prevent a negative number */
  height: number
  width: number
}

The Usual Solution

  • Lots of unit tests

  • Documentation

  • "Assume it is valid at this point in the code"

  • What happens if we delete a check somewhere?

  • What happens if the implementation changes?

Tests are a regression hazard. Documentation goes out of sync

A Different Solution

"Make illegal states unrepresentable" - Yaron Minsky

Some Tools

  • Group related things
  • String literal types
  • String literal types

Code: Union Types, Template Literal Types

ddd-with-ts/03-tools-grouping.ts
type Dimensions = {
  height: number
  width: number
}

export type Plank = {
  material: string

  serialNumber: string
  manufacturedDate: Date

  passedQA: boolean

  shipped: boolean
  shippingDate: Date

  dimensions: Dimensions
}

Impossible States

Is there anything we have overlooked?

passedQA: boolean
shipped: boolean

What are our states?

  • passedQA=true and shipped=false
  • passedQA=true and shipped=true
  • passedQA=false and shipped=false
  • passedQA=false and shipped=true

Boolean states are exponential

Explicit States

  • A product in QA
  • A product that has completed QA
    • Has been shipped
    • Not yet shipped

Modeling the desired state

  • Union types
ddd-with-ts/06-tools-union-types.ts
type Material = 'birch' | 'oak'

type Unit = 'mm' | 'm'

type Dimensions = {
  unit: Unit
  height: number
  width: number
}

type SerialNumber = `${Material}-${number}`

type Status =
  | {
      status: 'qa-needed'
    }
  | {
      status: 'ready-for-shipping'
      shipped: boolean
      shippingDate: Date
    }

export type Plank = Status & {
  material: Material

  serialNumber: SerialNumber
  manufacturedDate: Date

  dimensions: Dimensions
}

What do we see?

  • We actually notice that we have a missing state - what happens if QA does not pass?
ddd-with-ts/07-tools-add-missing-state.ts
type Material = 'birch' | 'oak'

type Unit = 'mm' | 'm'

type Dimensions = {
  unit: Unit
  height: number
  width: number
}

type SerialNumber = `${Material}-${number}`

type Status =
  | {
      status: 'qa-needed'
    }
  | {
      status: 'scrapped'
    }
  | {
      status: 'ready-for-shipping'
      shipped: boolean
      shippingDate: Date
    }

export type Plank = Status & {
  material: Material

  serialNumber: SerialNumber
  manufacturedDate: Date

  dimensions: Dimensions
}

Being 100% Sure

  • Can our dimensions be negative?
  • Need to validate this

Code: Option Type and Branded Type usage

ddd-with-ts/08-tools-branded-types.ts
type Unit = 'mm' | 'm'

type Dimensions = {
  unit: Unit
  height: PositiveNumber
  width: PositiveNumber
}

type Option<T> = T | undefined

type PositiveNumber = number & { __brand: 'PositiveNumber' }

const isPositiveNumber = (num: number): num is PositiveNumber => num > 0

const createDimensions = (
  unit: Unit,
  height: number,
  width: number
): Option<Dimensions> => {
  if (isPositiveNumber(height) && isPositiveNumber(width)) {
    return { unit, height, width }
  }

  return undefined
}

Why do this?

  • Interrogate the domain
  • Clarify intent
  • Reduces testing

References

END