Check your domain

13 March 2024

Updated: 08 April 2024

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
1
export type Plank = {
2
material: string
3
4
serialNumber: string
5
manufacturedDate: Date
6
7
passedQA: boolean
8
9
shipped: boolean
10
shippingDate: Date
11
12
height: number
13
width: number
14
}

Poke some holes

Code: What potential issues are there in our model

ddd-with-ts/02-domain-issues.ts
1
export type Plank = {
2
/** Is this just a string? */
3
material: string
4
5
/** Is this some random bit of text? Does it have a structure */
6
serialNumber: string
7
8
/** Are there any constraints on this Date? */
9
manufacturedDate: Date
10
11
passedQA: boolean
12
13
/** Could we ship something that did not pass QA */
14
shipped: boolean
15
shippingDate: Date
16
17
/** What units are these measured in? How do we prevent a negative number */
18
height: number
19
width: number
20
}

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
1
type Dimensions = {
2
height: number
3
width: number
4
}
5
6
export type Plank = {
7
material: string
8
9
serialNumber: string
10
manufacturedDate: Date
11
12
passedQA: boolean
13
14
shipped: boolean
15
shippingDate: Date
16
17
dimensions: Dimensions
18
}

Impossible States

Is there anything we have overlooked?

1
passedQA: boolean
2
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
1
type Material = 'birch' | 'oak'
2
3
type Unit = 'mm' | 'm'
4
5
type Dimensions = {
6
unit: Unit
7
height: number
8
width: number
9
}
10
11
type SerialNumber = `${Material}-${number}`
12
13
type Status =
14
| {
15
status: 'qa-needed'
16
}
17
| {
18
status: 'ready-for-shipping'
19
shipped: boolean
20
shippingDate: Date
21
}
22
23
export type Plank = Status & {
24
material: Material
25
26
serialNumber: SerialNumber
27
manufacturedDate: Date
28
29
dimensions: Dimensions
30
}

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
1
type Material = 'birch' | 'oak'
2
3
type Unit = 'mm' | 'm'
4
5
type Dimensions = {
6
unit: Unit
7
height: number
8
width: number
9
}
10
11
type SerialNumber = `${Material}-${number}`
12
13
type Status =
14
| {
15
status: 'qa-needed'
16
}
17
| {
18
status: 'scrapped'
19
}
20
| {
21
status: 'ready-for-shipping'
22
shipped: boolean
23
shippingDate: Date
24
}
25
26
export type Plank = Status & {
27
material: Material
28
29
serialNumber: SerialNumber
30
manufacturedDate: Date
31
32
dimensions: Dimensions
33
}

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
1
type Unit = 'mm' | 'm'
2
3
type Dimensions = {
4
unit: Unit
5
height: PositiveNumber
6
width: PositiveNumber
7
}
8
9
type Option<T> = T | undefined
10
11
type PositiveNumber = number & { __brand: 'PositiveNumber' }
12
13
const isPositiveNumber = (num: number): num is PositiveNumber => num > 0
14
15
const createDimensions = (
16
unit: Unit,
17
height: number,
18
width: number
19
): Option<Dimensions> => {
20
if (isPositiveNumber(height) && isPositiveNumber(width)) {
21
return { unit, height, width }
22
}
23
24
return undefined
25
}

Why do this?

  • Interrogate the domain
  • Clarify intent
  • Reduces testing

References

END