Better Configuration in TypeScript with the `satisfies` Operator
Tags: typescript, javascript, webdev
Published: 2023-01-17 18:16:57

There's a new and better way to do type-safe configuration in TypeScript.

It is with the new `satisfies` operator, released in TypeScript 4.9:

```tsx
type Route = { path: string; children?: Routes }
type Routes = Record<string, Route>

const routes = {
  AUTH: {
    path: "/auth",
  },
} satisfies Routes; // 😍
```

## Why we need `satisfies`

Let’s start by rewriting the above example with a standard type declaration.

```tsx
type Route = { path: string; children?: Routes }
type Routes = Record<string, Route>

const routes: Routes = {
  AUTH: {
    path: "/auth",
  },
}
```

Everything looks fine so far. We get proper type checking and completion in our IDE.

But, when we go to use the routes object, the compiler (and subsequently our IDE as well) has no idea what the actual configured routes are.

For instance, this compiles just fine, yet would throw errors at runtime:

```tsx
// 🚩 TypeScript compiles just fine, yet `routes.NONSENSE` doesn't actually exist
routes.NONSENSE.path
```

Why is this? It’s because our `Routes` type can take any string as a key. So TypeScript approves of any key access, including everything from simple typos to completely nonsense keys. We also get no help in our IDE to autocomplete the valid route keys as well.

You could say “well what about the `as` keyword”. So why not, we can briefly explore that too:

```tsx
type Route = { path: string; children?: Routes }
type Routes = Record<string, Route>

const routes = {
  AUTH: {
    path: "/auth",
  },
} as Routes
```

This is a common practice in TypeScript, but is actually quite dangerous.

Not only do we have the same issues as above, but you can actually write completely nonexistent key and value pairs and TypeScript will look the other way:

```tsx
type Route = { path: string; children?: Routes }
type Routes = Record<string, Route>

const routes = {
  AUTH: {
    path: "/auth",
    // 🚩 TypeScript compiles just fine, yet this is not even a valid property!
    nonsense: true,
  },
} as Routes
```

Broadly speaking, try to avoid using the `as` keyword in TypeScript in general.

## `Satisfies` saves the day

Now, we can finally rewrite our above examples with the new `satisfies` keyword:

```tsx
type Route = { path: string; children?: Routes }
type Routes = Record<string, Route>

const routes = {
  AUTH: {
    path: "/auth",
  },
} satisfies Routes
```

With this, we get all of the proper type checking we desire:

```tsx
routes.AUTH.path     // ✅
routes.AUTH.children // ❌ routes.auth has no property `children`
routes.NONSENSE.path // ❌ routes.NONSENSE doesn't exist
```

And, of course, autocomplete in our IDE:


![Screenshot showing `AUTH` as the proper autocomplete result for `routes.` in an IDE](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8j9owkh3p70o2hy048wi.png)



We can even take a slightly more complex example, like this:

```tsx
type Route = { path: string; children?: Routes }
type Routes = Record<string, Route>

const routes = {
  AUTH: {
    path: "/auth",
    children: {
      LOGIN: {
        path: '/login'
      }
    }
  },
  HOME: {
    path: '/'
  }
} satisfies Routes
```

And now we get IDE autocomplete and type checking all the way down the tree, precisely to how we have configured things, as opposed to based on the more general type we used:

![Gif showing proper IDE autocompletion now](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6eghehkg5whc0yfkt709.gif)

```tsx
routes.AUTH.path                // ✅
routes.AUTH.children.LOGIN.path // ✅
routes.HOME.children.LOGIN.path // ❌ routes.HOME has no property `children`
```

Exactly what we wanted.

## Combining with `as const`

One last situation you may run into is that with just a plain usage of `satisfies` , our configuration object is captured a little more loosely than might be ideal.

For instance, with this code:

```tsx
const routes = {
  HOME: { path: '/' }
} satisfies Routes
```

If we check the type of the `path` property, we get the type `string`

```tsx
routes.HOME.path // Type: string
```

But when it comes to configuration, this is where [const assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) (aka `as const`) have really shined. If we instead used `as const` here, we would get more precise types, down to the exact string literal `'/'`:

```tsx
const routes = {
  HOME: { path: '/' }
} as const

routes.HOME.path // Type: '/'
```

This may seem like “ok, that’s a neat trick but why do I care”, but let’s consider if we had a method like this that was type-safe all the way down to the exact paths it accepts:

```tsx
function navigate(path: '/' | '/auth') { ... }
```

If we just used `satisfies`, where each `path` is only known to be a `string`, we would get type errors here:

```tsx
const routes = {
  HOME: { path: '/' }
} satisfies Routes

navigate(routes.HOME.path) 
// ❌ Argument of type 'string' is not assignable to parameter of type '"/" | "/auth"'
```

Argh! `HOME.path` **is** a valid string (`'/'`), but TypeScript is saying it’s not.

Well, that’s where we can get the best of all worlds by *combining* `satisfies` and `as const` like so:

```diff
  const routes = {
    HOME: { path: '/' }
- } satisfies Routes
+ } as const satisfies Routes
```

Now, we have a great solution, with type checking all the way down to the exact literal values we used. Beautiful.

```tsx
const routes = {
  HOME: { path: '/' }
} as const satisfies Routes 

navigate(routes.HOME.path) // ✅ - as desired
navigate('/invalid-path')  // ❌ - as desired
```

So finally, you might ask, why not just use `as const` instead of `satisfies`?

Well, with just `as const`, we won’t get any type checking of our object itself at the time of authoring it. So that means no autocomplete in our IDE or warnings for typos and other issues at the time of writing.

This is why the combination here is so effective.

## Coming to libraries and frameworks near you

The biggest benefit you’ll likely get from this new operator is support in popular libraries and frameworks that you use.

For instance, with NextJS, if you previously wrote:

```tsx
export const getStaticProps: GetStaticProps = () => {
  return {
    hello: 'world'
  }
}

export default function Page(
  props: InferGetStaticPropsType<typeof getStaticProps>
) {
  // 🚩 Typo not caught, because `props` is of type `{ [key: string]: any }`
  return <div>{props.hllo></div>
}
```

`props` in this case is just `{ [key: string]: any }`. Sigh, not very helpful.

But with `satisfies`, you can instead write:

```tsx
export const getStaticProps = () => {
  return {
    hello: 'world'
  }
} satisfies GetStaticProps

export default function Page(
  props: InferGetStaticPropsType<typeof getStaticProps>
) {
  // 😍 `props` is of type `{ hello: string }`, so our IDE makes sure our
  // code is correct
  return <div>{props.hello}</div>
}
```

And with this you get all of the same type checking and completion that you did before, but now the proper props type `{ hello: string }`

## Conclusion

Typescript 4.9 introduced the new `satisfies` keyword, which is extremely handy for most configuration-related tasks in TypeScript. 

Compared to a standard type declaration, it can strike an elegant balance between type checking and understanding the exact details of your configuration for optimal type safety and a great in-IDE.

Thank you to [u/sauland](https://www.reddit.com/r/webdev/comments/zrt1rb/comment/j15fffv/?utm_source=share&utm_medium=web2x&context=3) for proposing this routes example, as well as to [Matt Pocock](https://www.youtube.com/watch?v=Danki1DyiuI) and [Lee Robinson](https://twitter.com/leeerob/status/1563540593003106306?lang=en) for the NextJS example. 

## 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.

You can read more about how this can improve your workflow [here](https://www.builder.io/blog/headless-cms-workflow).

You may find it interesting or useful:

![Builder.io Demo Gif](https://res.cloudinary.com/practicaldev/image/fetch/s--Y1qvC5Xj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/au5t8ti43cci5n6t6cfb.gif)