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
1export type Plank = {2 material: string3
4 serialNumber: string5 manufacturedDate: Date6
7 passedQA: boolean8
9 shipped: boolean10 shippingDate: Date11
12 height: number13 width: number14}
Poke some holes
Code: What potential issues are there in our model
1export type Plank = {2 /** Is this just a string? */3 material: string4
5 /** Is this some random bit of text? Does it have a structure */6 serialNumber: string7
8 /** Are there any constraints on this Date? */9 manufacturedDate: Date10
11 passedQA: boolean12
13 /** Could we ship something that did not pass QA */14 shipped: boolean15 shippingDate: Date16
17 /** What units are these measured in? How do we prevent a negative number */18 height: number19 width: number20}
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
1type Dimensions = {2 height: number3 width: number4}5
6export type Plank = {7 material: string8
9 serialNumber: string10 manufacturedDate: Date11
12 passedQA: boolean13
14 shipped: boolean15 shippingDate: Date16
17 dimensions: Dimensions18}
Impossible States
Is there anything we have overlooked?
1passedQA: boolean2shipped: boolean
What are our states?
passedQA=true
andshipped=false
passedQA=true
andshipped=true
passedQA=false
andshipped=false
passedQA=false
andshipped=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
1type Material = 'birch' | 'oak'2
3type Unit = 'mm' | 'm'4
5type Dimensions = {6 unit: Unit7 height: number8 width: number9}10
11type SerialNumber = `${Material}-${number}`12
13type Status =14 | {15 status: 'qa-needed'16 }17 | {18 status: 'ready-for-shipping'19 shipped: boolean20 shippingDate: Date21 }22
23export type Plank = Status & {24 material: Material25
26 serialNumber: SerialNumber27 manufacturedDate: Date28
29 dimensions: Dimensions30}
What do we see?
- We actually notice that we have a missing state - what happens if QA does not pass?
1type Material = 'birch' | 'oak'2
3type Unit = 'mm' | 'm'4
5type Dimensions = {6 unit: Unit7 height: number8 width: number9}10
11type SerialNumber = `${Material}-${number}`12
13type Status =14 | {15 status: 'qa-needed'16 }17 | {18 status: 'scrapped'19 }20 | {21 status: 'ready-for-shipping'22 shipped: boolean23 shippingDate: Date24 }25
26export type Plank = Status & {27 material: Material28
29 serialNumber: SerialNumber30 manufacturedDate: Date31
32 dimensions: Dimensions33}
Being 100% Sure
- Can our dimensions be negative?
- Need to validate this
Code: Option Type and Branded Type usage
1type Unit = 'mm' | 'm'2
3type Dimensions = {4 unit: Unit5 height: PositiveNumber6 width: PositiveNumber7}8
9type Option<T> = T | undefined10
11type PositiveNumber = number & { __brand: 'PositiveNumber' }12
13const isPositiveNumber = (num: number): num is PositiveNumber => num > 014
15const createDimensions = (16 unit: Unit,17 height: number,18 width: number19): Option<Dimensions> => {20 if (isPositiveNumber(height) && isPositiveNumber(width)) {21 return { unit, height, width }22 }23
24 return undefined25}
Why do this?
- Interrogate the domain
- Clarify intent
- Reduces testing