Type Driven Development: Non-Empty Collections

Written by

in

Part 4 – Non-Empty Collections

This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.

Now that we have the Big 3® types covered (Branded “a thing”, Product “this AND that”, and Union “this OR that”), let’s see what you can do when you combine them with type parameters; like function parameters, except for types.

Array’s often hold critical data you need, yet have a flaw: you can’t guarantee there is anything in the Array. This makes working with Arrays’ a bit painful: you have to check first to see if your item is there, THEN use it:

// check for null
if(data[0]) {
  …
}Code language: JavaScript (javascript)

This gets worse if the Array isn’t in order:

// find it first
const maybeItem = data.find( i => i.name === 'desire' )
// then check for null
if(maybeItem) {
  …
}Code language: JavaScript (javascript)

This comes up with UI’s that have multiple Accounts, or in API’s when loading Feature Flags; the use case for both is “give me the important data first, and the remaining data, I can use if I need too later”. But the data isn’t shaped like that; it’s a list you must look inside and verify what you found isn’t null. Wouldn’t it be nice if you ALWAYS had at least the data you were looking for, and the rest of the data if you needed it?

You have 2 options: a custom Product Type or an Array that can’t be empty.

First option, make a Product Type where the first is always there, and the rest can be empty or full of items:

type AtLeast1 = { item: ?, data: Array }Code language: JavaScript (javascript)

But… what type of item is there, and full of what type of items? For that, we need a parameter to customize the type to whatever we need, so we’ll add a type parameter, usually called “T” which is short for “Type” (yes, it’s uppercased unlike TypeScript/Python/Lua/Ruby/etc function parameters). In TypeScript, Luau, and other languages, parameters are usually parentheses like (one, two), whereas type parameters use the less than and greater than symbols like .

type AtLeast1 = { item: T, data: Array<T> }Code language: HTML, XML (xml)

Using it:

type AtLeast1Account = AtLeast1

Now, you’re guaranteed to have the data you need, but the rest is there if you need it:

// ✅ good
const account1 = { 
  item: primaryAccount, 
  data: [ secondaryAccount ] 
}

// ✅ also good
const account2 = { 
  item: anotherPrimaryAccount, 
  data: [] 
}Code language: JavaScript (javascript)

The other option… define an Array where it HAS TO HAVE at least 1 item:

type NonEmptyArray = [ T, …Array[T] ]

// ❌ does not compile
const empty:NonEmptyArray<string> = []

// ✅ compiles
const hasOne:NonEmptyArray<string> = ["🐮"]Code language: PHP (php)

You can see now that you have the basic core types of Branded, Product, and Union/Discriminated Unions, once you combine with Type Parameters, you can create all kinds of types to ensure the data is in the shape of what you need and the types are easier to work with while flexible for your needs.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *