Hi! I’m Sam, and I write interactive blog posts about computer science. Because that doesn’t quite pay the bills, during the day I’m a senior software engineer for Budibase, an open source, low-code, self-hostable app building platform. We make heavy use of TypeScript, both on the front-end and the back-end, and in this post I’d like to walk you through some real-world use-cases we have for TypeScript’s “utility types.”
What is a utility type?permalink
Utility types are types that modify other types. You can think of them as functions, but they operate on types instead of values. It’s mind bending at first — especially if you’re coming from languages that don’t have anything similar — but we’re going to walk through a load of examples to see how they work.
TypeScript has a bunch of built-in utility types, such as Uppercase
:
- Code language
- ts
type Name = "Alice" | "Bob" type UppercasedName = Uppercase<Name> // type UppercasedName = "ALICE" | "BOB"
Our Name
type here only allows the strings "Alice"
or "Bob"
. This highlights an important difference between TypeScript and other typed languages. In TypeScript, a type describes a set of values. For example, the string
type describes all possible strings. number
describes all numbers JavaScript can work with. "Alice" | "Bob"
describes just the two strings "Alice"
and "Bob"
. The set of values a type describes is called that type’s “value space.”
The Uppercase
utility type takes a type that describes a set of strings, and converts those strings to their uppercase variants. It’s like a function that takes a type as input, and produces a new type as output.
What I’m going to do in the rest of this post is show you examples of utility types you can find in the Budibase codebase. These are real utility type use-cases that we depend on every day.
Partialpermalink
Partial
takes a type and makes all of its properties optional.
- Code language
- ts
interface User { id: string name: string } type PartialUser = Partial<User> // PartialUser = { // id?: string // name?: string // }
According to GitHub’s code search on the budibase/budibase repo, we use Partial
in 45 files. It’s the most used utility type in our codebase, and I’m going to talk about 2 ways we use it.
Patch updates
At Budibase we use Koa as our web server framework. An example handler for a route in our codebase looks like this:
- Code language
- ts
async function find(ctx: UserCtx<void, FindTableResponse>) { const tableId = ctx.params.tableId const table = await sdk.tables.getTable(tableId) ctx.body = table }
This endpoint fetches a specific Table
by its tableId
.
The UserCtx
type signifies that this is an authenticated request, and the types inside of UserCtx
are the request and response types. The request type is void
because this is a GET handler and has no request body. The response type is FindTableResponse
.
For some of our endpoints that update database records, you might see a request type defined like this:
- Code language
- ts
interface UpdateAppRequest extends Partial<App> {}
Our handler that updates App
objects allows you to write to any field in App
. It would be possible to define a whole new type for this purpose, with all of the same properties as App
except they’re all optional, but it’s much easier to use the Partial
utility type to do it for us.
The actual endpoint is quite complicated, but you can boil it down to this:
- Code language
- ts
async function update(ctx: UserCtx<UpdateAppRequest, UpdateAppResponse>) { const appId = ctx.params.appId const app = await sdk.applications.getApp(appId) const updatedApp = { ...app, ctx.request.body } const savedApp = await sdk.applications.saveApp(updatedApp) ctx.body = savedApp }
If you only wanted a subset of fields to be updatable, you could split your type into editable and non-editable fields.
- Code language
- ts
interface UserReadonly { id: string createdAt: Date } interface UserEditable { name: string } type User = UserReadonly & UserEditable type UpdateUserRequest = Partial<UserEditable> type UpdateUserResponse = User
The benefit to using Partial
this way is that when you’re adding a new field to a type, you don’t need to remember to add the field in multiple locations.
Partial Record
We’re going to touch on another utility type here, the Record
. A Record
allows you to describe an object’s keys and values by referencing other types.
- Code language
- ts
type Name = "Alice" | "Bob" const likesJazz: Record<Name, boolean> = { Alice: true, Bob: false, }
You might be wondering why we don’t just define likesJazz
like this:
- Code language
- ts
const likesJazz = { Alice: true, Bob: false, }
It’s a good question, the result is identical. But what happens when you add a new string to Name
?
- Code language
- ts
type Name = "Alice" | "Bob" | "Brett" // Type error: Property 'Brett' is missing const likesJazz: Record<Name, boolean> = { Alice: true, Bob: false, } // No error const likesJazz = { Alice: true, Bob: false, }
This makes Record
s great for specifying a 1:1 mapping between types.
But what if you’re okay with only mapping some of the values? Maybe there’s a sensible default and you only want to map a couple of exceptions to the rule.
We do this here:
- Code language
- ts
export type UIDatasourceType = | "table" | "view" | "viewV2" | "query" | "custom" | "link" | "field" | "jsonarray" const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = { viewV2: "view", } function friendlyName(datasourceType: UIDatasourceType): string { return friendlyNameByType[datasourceType] || datasourceType }
In most cases, the string of the UIDatasourceType
is fine to use, but when we introduced a new version of one of the types we didn’t want to surface that V2
suffix to users.
Why not use a plain object instead of a partial record? Isn’t the benefit of a partial record that you can ignore some of the keys?
If we did that, we’d be allowing ourselves to make mistakes. What if we’d written this?
- Code language
- ts
const friendlyNameByType = { veiwV2: "view", }
In case you missed it, there’s a typo in the key: veiwV2
instead of viewV2
. If we use a plain object, we don’t get type safety on the keys. A partial record gives us that type safety, and flexibility, to address this specific concern.
Downsides
The main problem with the standard library Partial
is that it only goes one level deep. If you have an object that embeds another object within it, using Partial
will do nothing to the embedded object.
- Code language
- ts
interface User { id: string preferences: { theme: string } } type PartialUser = Partial<User> // PartialUser = { // id?: string // preferences?: { // theme: string // } // }
Note that preferences.theme
is not optional.
To solve this, we could make use of a library like ts-toolbet. The Partial type in ts-toolbelt
supports “deep” partials.
- Code language
- ts
import { O } from 'ts-toolbelt' type PartialUser = O.Partial<User, "deep"> // PartialUser = { // id?: string // preferences?: { // theme?: string // } // }
Alternately, if you don’t want to bring in a new dependency for just this one type, you can create your own DeepPartial
type like we did.
- Code language
- ts
export type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] }
It’s outside of the scope of this article to talk about how this works, but I’ll demonstrate that it does indeed work.
- Code language
- ts
type DeepPartialUser = DeepPartial<User> // DeepPartialUser = { // id?: string // preferences?: { // theme?: string // } // }
Omitpermalink
Omit
removes properties from a type.
- Code language
- ts
interface Planet { name: string numMoons: number } type MoonlessPlanet = Omit<Planet, "numMoons"> // MoonlessPlanet = { // name: string // }
Unsaved records
Where we use this the most in Budibase is to create types that represent our database records, except they haven’t been saved to the database yet. In this situation, the type will not have an id
property.
An example of this is in saving our ViewV2
type:
- Code language
- ts
export async function create( tableId: string, viewRequest: Omit<ViewV2, "id" | "version"> ): Promise<ViewV2> { // ... }
The version
property is one that we populate internally, it’s not exposed through our API. So we omit it, as well as the id
property.
The benefit to this is similar to the benefit of using Partial
to describe the type used to update a database record: you can maintain a single source of truth for the shape of the type, and use utility types to modify it for specific purposes. In this case, if we add a new field to the type then our function that creates new database records of that type is updated automatically.
ReturnTypepermalink
ReturnType
gets the return type of a function type.
- Code language
- ts
function add(a: number, b: number): number { return a + b } type AddReturn = ReturnType<typeof add> // AddReturn = number
Compatibility across the browser and NodeJS
Budibase uses TypeScript on the front-end and the back-end. We use a monorepo structure split into 13 packages. Some of these packages are intended to run in a browser, others in NodeJS, and then a couple of them are expected to run in both environments.
One of the considerations when writing TypeScript that needs to work in multiple environments is that the types of built-in functions don’t always match. An example of this is the setTimeout
function. In the browser, it returns a number
. In NodeJS, it returns a Timeout
instance.
We have a debounce
function that we’ve written in a way that will work regardless where it is run.
- Code language
- ts
export const debounce = (callback: Function, minDelay = 1000) => { let timeout: ReturnType<typeof setTimeout> return async (...params: any[]) => { return new Promise(resolve => { if (timeout) { clearTimeout(timeout) } timeout = setTimeout(async () => { resolve(await callback(...params)) }, minDelay) }) } }
Admittedly, this code isn’t the best example of a debounce
function. The use of the Function
type is discouraged — calling it returns any
— and the any[]
for the parameters of the returned function could be improved. I almost didn’t include this example for these reasons, but decided to leave it in as a nod to the reality that not all the code written commercially is going to be perfect. This works well enough. I wouldn’t say no to a PR improving it, though!
Readonlypermalink
Readonly
takes a type and marks all of its properties as readonly
, so that any attempt to assign to them results in a TypeScript error.
- Code language
- ts
interface Point { x: number y: number } function resetX(point: Readonly<Point>) { point.x = 0 // Cannot assign to 'x' because it is a read-only property. }
Communicating intent
The way that we use this in Budibase feels more like documentation than anything else. When we want to signal to the caller of a function that we are going to return a new copy of something without modifying it, we use Readonly
.
A good example of this is our ensureQueryUISet
function:
- Code language
- ts
export function ensureQueryUISet(viewArg: Readonly<ViewV2>): ViewV2 { const view = cloneDeep<ViewV2>(viewArg) // [...] business logic that modifies `view` return view }
By reading the signature of this function, you know straight away that it must copy the input if it plans to make changes to it. Then in future, if anyone comes to modify this function they will also have compile-time errors if they break that contract.
A caveat to this is that Readonly
suffers from the same problem that Partial
does: it only affects the first level of properties. It’s shallow, not deep.
Fortunately, ts-toolbelt
has our back again:
- Code language
- ts
import { O } from "ts-toolbelt" type ReadonlyViewV2 = O.Readonly<ViewV2, keyof ViewV2, "deep">
For some reason, O.Readonly
takes 3 arguments where O.Partial
takes 2. The 2nd argument of O.Readonly
is which keys to mark as readonly
, so above I’ve passed in all keys of the type we’re working with.
If you wanted to, you could create your own utility type to make this easier:
- Code language
- ts
import { O } from "ts-toolbelt" type DeepReadonly<T extends object> = O.Readonly<T, keyof T, "deep"> type ReadonlyViewV2 = DeepReadonly<ViewV2>
Wrapping uppermalink
There are plenty more utility types in the standard library and in libraries like ts-toolbelt. I picked the ones we make the most use of at Budibase, but I’m confident we could be using more of them! They’re one of my favourite features of TypeScript, and I hope this post has given you some inspiration for how you could use them in your own code.