Strongly typed Id in f#

Using a primitive type to store ids is inherently problematic. I explore the minimal way of creating a type-strong Id object in f#.

Motivation

Way too often I see code where someone defines an id as a simple int or Guid or something along those lines. This is a classic case of the Primitive Obsession smell and has a variety of potential problems, as well as may pollute your code with Guard clauses.

I am in the process of trying to create a Kanban board to explore Event Sourcing and Domain-Driven Design and learn F#. Here I need an Id on my Board type, to put on Events and pass around.

In-depth motivation

You might be able to skip this part. These are just my personal key points from the articles linked above. Reasons to have a type-strong id:

  • You don't want to be able to pass a ColumnId in a context where a BoardId is required.
  • Whether a BoardId is a Guid, int, string, or something else, is an implementation detail. Any consumer of the domain doesn't need/want to know.
  • It improves readability. It's quite obvious what kind of Id a method takes if it is strongly typed.

  • If it's an int, you probably don't want to allow a negative integer. Moving this validation into a BoardId type is nice.

The Solution

Initial (WRONG) solution

I initially expected to be able to do this.

type BoardId = Guid

It seemed like it would work initially, however you can then cast them back and forth implicitly. This is simply a type alias, which doesn't solve any problems regarding actual encapsulation, misuse or validation.

The (Minimal) Solution

After searching for some time I found this StackOverflow post

[<Struct>]
type ProductId = ProductId of Guid

The (Better) Solution

Improving a bit on their result (which included helper methods), I've added additional helpers and made this a bit stronger, covering the use cases that I had.

namespace Fanban.Domain

open System

[<Struct>]
type BoardId =
    private
    | BoardId of Guid

    static member New() = BoardId(Guid.NewGuid())
    static member Parse (value: string) = BoardId(Guid.Parse(value))
    static member TryParse (value: string) =
        let couldParse, result = Guid.TryParse(value)
        if couldParse then Some (BoardId result) else None
    member this.Value = let (BoardId i) = this in i
    override this.ToString() = this.Value.ToString()

This makes it possible to write code like:

let CreateBoardEvent name (columns: ColumnName list) =
    { Id = BoardId.New()
      Name = name
      ColumnNames = columns }

and types can define the id type very explicitly:

and SetBoardNameEvent =
    { BoardId: BoardId
      Name: string }

Did you find this article valuable?

Support Casper Weiss Bang's blog by becoming a sponsor. Any amount is appreciated!