Learn why Gartner just named Builder a Cool Vendor

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

TypeScript

TypeScript Utility Types You Need to Know

February 3, 2023

Written By Steve Sewell

Are you ever building something in TypeScript and realize...

Screen Shot 2023-02-03 at 12.48.22 PM.png

AGH! This package is not exporting a type I need!

Fortunately, TypeScript gives us a number of utility types that can solve this common problem.

For instance, to grab the type returned from a function, we can use the ReturnType utility:

import { getContent } from '@builder.io'
const content = await getContent()
// 😍
type Content = ReturnType<typeof getContent>

But we have one little problem. getContent is an async function that returns a promise, so currently our Content type is actuallyPromise, which is not what we want.

For that, we can use the Awaited type to unwrap the promise and get the type of what the promise resolves to:

import { getContent } from '@builder.io'
const content = await getContent()
// ✅
type Content = Awaited<ReturnType<typeof getContent>>

Now we have exactly the type we needed, even though it is not explicitly exported. Well, that’s a relief.

But what if we need argument types for that function?

For instance, getContent takes an optional argument called ContentKind that is a union of strings. I really don’t want to have to type this out manually, so let’s use the Parameters utility type to extract its parameters:

type Arguments = Parameters<typeof getContent>
// [ContentKind | undefined]

Parameters gives you a tuple of the argument types, and you can pull out a specific parameter type by index like so:

type ContentKind = Parameters<typeof getContent>[0]

But we have one last issue. Because this is an optional argument, our ContentKind type right now is actually ContentKind | undefined, which is not what we want.

For this, we can use the NonNullable utility type, to exclude any null or undefined values from a union type.

// ✅
type ContentKind = NonNullable<Parameters<typeof getContent>[0]>
// ContentKind

Now our ContentKind type perfectly matches the ContentKind in this package that was not being exported, and we can use it in our processContent function like so:

import { getContent } from '@builder.io'

const content = await getContent()

type Content = Awaited<ReturnType<typeof getContent>>
type ContentKind = NonNullable<Parameters<typeof getContent>[0]>

// 🥳
function processContent(content: Content, kind: ContentKind) {
  // ...
}

Utility Types with React

Utility types can also help us a lot with our React components.

For instance, below I have a simplistic component to edit calendar events, where we maintain an event object in state and modify the event title on change.

Can you spot the state bug in this code?

import React, { useState } from 'react'

type Event = { title: string, date: Date, attendees: string[] }

// 🚩
export function EditEvent() {
  const [event, setEvent] = useState<Event>()
  return (
    <input 
      placeholder="Event title"
      value={event.title} 
      onChange={e => {
        event.title = e.target.value
      }}
    />
  )
}

Doh, we are mutating the event object directly.

This will cause our input to not work as expected because React will not be aware of the state change and subsequently not rerender.

// 🚩
event.title = e.target.value

What we need to be doing is calling setEvent with a new object.

But wait, why didn’t TypeScript catch that?

Well, technically you can mutate objects with useState. You just basically never should. We can improve our type safety here by using the Readonly utility type, to enforce that we should not be mutating any properties on this object:

// ✅
const [event, setEvent] = useState<Readonly<Event>>()

Now our prior bug will be caught for us automatically, woo!

export function EditEvent() {
  const [event, setEvent] = useState<Readonly<Event>>()
  return (
    <input 
      placeholder="Event title"
      value={event.title} 
      onChange={e => {
        event.title = e.target.value
        //   ^^^^^ Error: Cannot assign to 'title' because it is a read-only property
      }}
    />
  )
}

Now, when we update our code to copy the event as needed, TypeScript is happy again:

<input
  placeholder="Event title"
  value={event.title} 
  onChange={e => {
    // ✅
    setState({ ...event, title: e.target.value })
  }}
/>

But, there is still a problem with this. Readonly only applies to top level properties of the object. We can still mutate nested properties and arrays without errors:

export function EditEvent() {
  const [event, setEvent] = useState<Readonly<Event>>()
  // ...

  // 🚩 No warnings from TypeScript, even though this is a bug
  event.attendees.push('foo')
}

But, now that we are aware of Readonly, we can combine that with its sibling ArrayReadonly, and a little bit of magic, and make our own DeepReadonly type like so:

export type DeepReadonly<T> =
  T extends Primitive ? T :
  T extends Array<infer U> ? DeepReadonlyArray<U> :
  DeepReadonlyObject<T>

type Primitive = 
  string | number | boolean | undefined | null

interface DeepReadonlyArray<T> 
  extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}

Thanks to Dean Merchant for the above code snippet.

Now, using DeepReadonly, we cannot mutate anything in the entire tree, preventing a whole range of bugs that could occur.

export function EditEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  // ...

  event.attendees.push('foo')
  //             ^^^^ Error!
}

Which will only pass type check if treated properly immutably:

export function EditEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  
  // ...
  
  // ✅
  setEvent({
    ...event,
    title: e.target.value,
    attendees: [...event.attendees, 'foo']
  })
}

One additional pattern you may want to use for this kind of complexity is to move this logic to a custom hook, which we can do like so:

function useEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  function updateEvent(newEvent: Event) {
    setEvent({ ...event, newEvent })
  }
  return [event, updateEvent] as const
}

export function EditEvent() {
  const [event, updateEvent] = useEvent()
  return (
    <input 
      placeholder="Event title"
      value={event.title} 
      onChange={e => {
        updateEvent({ title: e.target.value })
      }}
    />
  )
}

This allows us to simply provide the properties that have changed and the copying can be managed automatically for a nice DX and safety guarantees.

But we have a new problem. updateEvent expects the full event object, but what we intend is to only have a partial object, so we get the following error:

updateEvent({ title: e.target.value })
// 🚩       ^^^^^^^^^^^^^^^^^^^^^^^^^ Error: Type '{ title: string; }' is missing the following properties from type 'Event': date, attendees

Fortunately, this is easily solved with the Partial utility type, which makes all properties optional:

// ✅
function updateEvent(newEvent: Partial<Event>) { /* ... */ }
// ...
// All clear!
updateEvent({ title: e.target.value })

Alongside Partial, it’s worth also knowing the Required utility type, which does the opposite - takes any optional properties on an object and makes them all required.

Or, if we only want certain keys to be allowed to be included in our updateEvent function, we could use the Pick utility type to specify the allowed keys with a union:

function updateEvent(newEvent: Pick<Event, 'title' | 'date'>) { /* ... */ }
updateEvent({ attendees: [] })
//          ^^^^^^^^^^^^^^^^^ Error: Object literal may only specify known properties, and 'attendees' does not exist in type 'Partial<Pick<Event, "title" | "date">>'

Or similarly, we can use Omit to omit specified keys:

function updateEvent(newEvent: Omit<Event, 'title' | 'date'>) { /* ... */ }
updateEvent({ title: 'Builder.io conf' })
// ✅        ^^^^^^^^^^^^^^^^^ Error: Object literal may only specify known properties, and 'title' does not exist in type 'Partial<Omit<Event, "title">>'

We touched on a good bit of typescript utilities here! But here is a quick overview of the remaining ones, which are all pretty useful in my opinion.

Record<KeyType, ValueType>

Easy way to create a type representing an object with arbitrary keys that have a value of a given type:

const months = Record<string, number> = {
  january: 1,
  february: 2,
  march: 3,
  // ...
}

Exclude<UnionType, ExcludedMembers>

Removes all members from a union that are assignable to the ExcludeMembers type.

type Months = 'january' | 'february' | 'march' | // ...
type MonthsWith31Days = Exclude<Months, 'april' | 'june' | 'september' | 'november'>
// 'january' | 'february' | 'march' | 'may' ...

Extract<Union, Type>

Removes all members from a union that are not assignable to Type.

type Extracted = Extract<string | number, (() => void), Function>
// () => void

Just like Parameters, but for constructors:

class Event {
  constructor(title: string, date: Date) { /* ... */ }
}
type EventArgs = ConstructorParameters<Event>
// [string, Date]

Gives you the instance type of a constructor.

class Event { ... }
type Event = InstanceType<typeof Event>
// Event

Gives you the type of the this parameter for a function, or unknown if none is provided.

function getTitle(this: Event) { /* ... */ }
type This = ThisType<typeof getTitle>
// Event

Removes the this parameter from a function type.

function getTitle(this: Event) { /* ... */ }
const getTitleOfMyEvent: OmitThisParameter<typeof getTitle> = 
  getTitle.bind(myEvent)

Utility types in TypesScript are useful. Use them.

Hi! I'm Steve, CEO of Builder.io.

We make a way to drag + drop with your components to create pages and other CMS content on your site or app, visually.

You may find it interesting or useful:

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual CopilotGet a demo

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual CopilotGet a demo
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
ai8 MIN
The Big Lie AI Vendors Keep Telling You
November 27, 2024
AI8 MIN
Generate Figma Designs with AI
November 25, 2024
Design to code5 MIN
Builder.io Named a Cool Vendor in the 2024 Gartner® Cool Vendors™ in Software Engineering: User Experience
November 21, 2024