TypeScript Utility Types You Need to Know
Tags: typescript, javascript, webdev
Published: 2023-02-03 12:41:06

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

![Screenshot of code that needs a type that is not exported from a package](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i78upcdmbocj873ibi16.png)

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

Fortunately, TypeScript gives us a number of [utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) that can solve this common problem.

For instance, to grab the type returned from a function, we can use the [`ReturnType`](https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype) utility:

```tsx
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 actually`Promise<Content>`, which is not what we want.

For that, we can use the [`Awaited`](https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype) type to unwrap the promise and get the type of what the promise resolves to:

```tsx
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`](https://www.typescriptlang.org/docs/handbook/utility-types.html#nonnullabletype) utility type to extract its parameters:

```tsx
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:

```tsx
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`](https://www.typescriptlang.org/docs/handbook/utility-types.html#nonnullabletype) utility type, to exclude any `null` or `undefined` values from a union type.

```tsx
// ✅
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:

```tsx
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?

```tsx
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.

```tsx
// 🚩
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`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype) utility type, to enforce that we should not be mutating any properties on this object:

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

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

```tsx
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:

```tsx
<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:

```tsx
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:

```tsx
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](https://github.com/microsoft/TypeScript/issues/13923#issuecomment-372258196).*

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

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

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

Which will only pass type check if treated properly immutably:

```tsx
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:

```tsx
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:

```tsx
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`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype) utility type, which makes all properties optional:

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

Alongside `Partial`, it’s worth also knowing the [`Required`](https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype) 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:

```tsx
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:

```tsx
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">>'
```

## Moar utilities

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>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers)

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

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

### [Exclude<`UnionType, ExcludedMembers>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers)

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

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

### [Extract<`Union, Type>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union)

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

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

### [`ConstructorParameters<Type>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterstype)

Just like [Parameters](https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype), but for constructors:

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

### [`InstanceType<Type>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype)

Gives you the instance type of a constructor. 

```tsx
class Event { ... }
type Event = InstaneType<typeof Event>
// Event
```

### [`ThisParameterType<Type>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#thisparametertypetype)

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

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

### [`OmitThisParameter<Type>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omitthisparametertype)

Removes the `this` parameter from a function type.

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

## Conclusion

Utility types in TypesScript are useful. Use them.

## About me

Hi! I'm [Steve](https://twitter.com/Steve8708?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor), CEO of [Builder.io](https://www.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](https://www.builder.io/blog/headless-cms-workflow).

So this:

```tsx
import { BuilderComponent, registerComponent } from '@builder.io/react'
import { Hero, Products } from './my-components'

// Dynamically render compositions of your components
export function MyPage({ json }) {
  return <BuilderComponent content={json} />
}

// Use your components in the drag and drop editor
registerComponent(Hero)
registerComponent(Products)
```

Gives you this:

![Gif of Builder.io](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/au5t8ti43cci5n6t6cfb.gif)