Entities

This project and documentation are work in progress

Example code: src/lib/entities

Entities are where we encode the types in our system. You might call these 'models' or 'domain objects'. We also store types related to entities, such as the objects used to create / update them.

It's time for our first proper look at the example code. Open the entities folder and you'll see encodings for all the types required by our example system - a system for a "community library", with users, books and loans:

- Book.ts
- Loan.ts
- User.ts
- cast.ts
- index.ts

Let's dive right into the Book type:

import * as t from 'io-ts'
import { UUID } from 'io-ts-types/lib/UUID'
import { $cast } from './cast'

// IO-TS Codecs
// ---------------------------------------------------------------

export const BookInput = t.exact(t.type({
  name: t.string
}))

export const Book = t.exact(
  t.type({
    id: UUID,
    name: t.string
  })
)


// Casts
// ---------------------------------------------------------------

export const castBook = $cast(Book)
export const castBookInput = $cast(BookInput)


// Static types
// ---------------------------------------------------------------

export type Book = t.TypeOf<typeof Book>
export type BookInput = t.TypeOf<typeof BookInput>

There's a lot going on here. Let's break the code down:

What is io-ts and all this t.type business?

IO-TS is a library for defining types that can be verified at runtime, which it calls 'codecs'. You can derive TypeScript types from them too, meaning that you can now 'prove' your TypeScript types are an accurate representation of the data being received.

Naturally, this makes your project much more robust, because you know real-world data won't sneak past your type system

Codecs are pretty easy to write and map quite transparently to TypeScript types.

For instance, if you have a codec like this:

const Person = t.type({
  name: t.string,
  age: t.number
})

You can derive a typescript static type:

type Person = t.TypeOf<typeof Person>

where type Person equivalent to {
  name: string,
  age: number
}

But you can also derive a runtime check. Here I'm using a util $cast that's defined in my project:

const castPerson = $cast(Person)

const returns_a_person = castPerson({
  name: 'Ned Flanders',
  age: 56
}) // OK!

const throws_an_error = castPerson({
  age: 100
}) // Throws an error that 'name:string' is missing!

You can find the $cast helper in cast.ts

Whenever you see a function beginning with a dollar in this project, it signifies a higher-order function that itself returns a function.

For instance, a function named $foo will itself return another function called foo. You don't have to use this convention, but I find it makes HOFs a little easier to spot and reason about.

Check out the io-ts-types project for some very helpful extra types, like UUID or timezoned dates.

Do I have to use io-ts? What if I just want to use plain JS?

Not at all - you can use plain TypeScript types and interfaces if you prefer. That said, being able to validate your types is a huge boost to your ability to clamp down on bugs and unpredictable behaviour, so I'd recommend giving it a go. There are also libraries like joi you can use.

If you're using plain JS and don't care about validation, you might just write types in JSDoc or even skip the entities folder entirely (though this will make working with operations a little harder, as the types will be less obvious).

Back to the book types

Now we know what IO-TS does, we can deconstruct the Book.ts module:

  • There's a Book, which obviously encodes a book a user can borrow

  • There's a BookInput, which is used during Book CRUD

  • There are casts for both of the above 'codecs'

  • There are static types derived from both codecs

The index.ts file

At the root of the entities folder should be an index.ts file that exports all the static types and casts defined within each module. This will itself then be exported at the library root so that adapters and bindings can import domain types.

Last updated