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
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
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
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=trueandshipped=falsepassedQA=trueandshipped=truepassedQA=falseandshipped=falsepassedQA=falseandshipped=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
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?
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
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