Primitive obsession

and how to fight it with TypeScript

Tomasz Ducin  •      @tomasz_ducin  •  ducin.dev

Experience is the name everyone gives to their mistakes.

- Oscar Wilde

@tomasz_ducin

Hi, I'm Tomasz

Independent Consultant & Software Architect

Trainer, Speaker, JS/TS Expert

ArchitekturaNaFroncie.pl (ANF)

Warsaw, PL

tomasz (at) ducin.dev

@tomasz_ducin

Primitive Obsession

let productsCount = 12 // count
let unitPrice = 1.99 // money

productsCount = 13 // 👍
productsCount = unitPrice // 🤯

let discount = 0.99
productsCount -= discount // 🤯

unitPrice*unitPrice // 🤯
productsCount = unitPrice * discount // 🤯

@tomasz_ducin

Fix the cause, not the symptom.

- Steve Maguire

@tomasz_ducin

Type-Safety is configurable.

@tomasz_ducin

Structural Typing FTW.

Type Aliases

type Money = number

let unitPrice: Money = 1.99

declare function totalPrice(
  productPrice: Money,
  productQuantity: number
): void

totalPrice(unitPrice, 12) // 👍
totalPrice(1.99, 12) // 🤯

@tomasz_ducin

Brand/Opaque Types

declare const brand_symbol: unique symbol
type BrandType<BaseType, BrandName> = BaseType & {
  readonly [brand_symbol]: BrandName
}

type Money = BrandType<number, "MONEY">
type DateString = BrandType<string, "DATE_STRING">
type EmailString = BrandType<string, "EMAIL">

let myAddress: EmailString = 'tomasz@ducin.it' // ❌
  // Type 'string' is not assignable to type 'EmailString
const asEmail = (arg: string) => arg as EmailString
let myAddress: EmailString = asEmail('tomasz@ducin.it') // 👍

@tomasz_ducin

Brand/Opaque Types

declare function sendEmail(email: EmailString): void;

let myAddress = 'tomasz@ducin.it' // string
sendEmail(myAddress) // ❌
  // Type 'string' is not assignable to type 'EmailString'.

function isEmail(arg: string): arg is EmailString {
  const regexp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
  return regexp.test(arg);
}

if (isEmail(myAddress)){
  // inside: myAddress is EmailString
  sendEmail(myAddress) // 👍
} // else: just a string

@tomasz_ducin

no place for business logic...

let some = {
  amount: 19.99,
  currency: "PLN"
}
let more = {
  amount: 50,
  currency: "USD"
}

let sum
if (some.currency == more.currency){
  sum = {
    amount: some.amount + more.amount,
    currency: some.currency
  }
}

@tomasz_ducin

Value Objects (DDD)

class Money {
  private constructor(
    private amount: number,
    private currency: string,
  ){}

  static from(amount: number, currency: Currency){
    return new Money(amount, currency)
  }
  
  nominalValue(){
    return this.amount
  }

  //...
}
let money = Money.from(19.99, "PLN")

@tomasz_ducin

Value Objects (DDD)

class Money {
  private amount: number,
  private currency: string,
  //...

  add(another: Money){
    if (this.currency != another.currency){
      throw new Error('Cannot add different currencies')
    }
    return new Money(this.amount + another.amount, this.currency)
  }

  multiply(factor: number){
    return new Money(this.amount * factor, this.currency)
  }
}

@tomasz_ducin

Which way to go?

@tomasz_ducin

number, string, ...
type Money = number
type Money = BrandType<number, "MONEY">

Primitives everywhere

Brand/Opaque Types

Type Aliases

Value Objects (DDD)

class Money {
  constructor(
    private amount: number,
    private currency: string,
  ){}
  
  //...
}

Fix the cause, not the symptom.

- Steve Maguire

@tomasz_ducin

WE STAND

WITH YOU

Thank you

Tomasz Ducin  •     @tomasz_ducin  •  ducin.it