Keeping TypeScript Type Guards safe and up to date (a simpler solution)
Tags: typescript, typeguard
Published: 2022-04-26 10:55:17

_This article is about improving the solution shared by [Michal Szorad](https://twitter.com/michalszorad) in his [TypeScript: Keeping Type Guards Safe and Up To Date article](https://levelup.gitconnected.com/typescript-keeping-type-guards-safe-and-up-to-date-2457d52bd722)._

_Photo by <a href="https://unsplash.com/@sushioutlaw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Brian McGowan</a> on <a href="https://unsplash.com/s/photos/guards?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>_

---

Type guards allow us to check that a runtime, unknown object (think of data coming from the back-end) respects a given type. Type guards are nothing more than written-by-developer functions that receive an `unknown` parameter, check that all the properties are there, and respect a specific type. Hence, TypeScript can safely assume the parameter is typed correctly.

An example: given this type
```ts
interface Person {
  name: string
  age: number
}
```

and this is a type guard for the above `Person`
```ts
function isPerson(value: unknown): value is Person {
  return (
    typeof value === 'object' &&
    value &&
    value.hasOwnProperty('name') &&
    value.hasOwnProperty('age') &&
    typeof value.name === 'string' &&
    typeof value.age === 'number'
  )
}
```

What is the main problem with the above type guard? It does not scale. If you update the `Person` type, TypeScript does not throw. Imagine changing the type to 
```ts
interface Person {
  name: string
  age: number
  something: string // <-- a new property
}
```

TypeScript does not complain, but our `isPerson` function is now outdated.

[Michal Szorad](https://twitter.com/michalszorad) already discussed this problem in his [TypeScript: Keeping Type Guards Safe and Up To Date](https://levelup.gitconnected.com/typescript-keeping-type-guards-safe-and-up-to-date-2457d52bd722) article (I stole the title, sorry, Michal) and proposed a scaling working solution.

Please read his article to understand the problem and its solution fully. This article uses his approach with some slight differences that come to a more straightforward solution (in my opinion). The differences are:
1. Using an `isPlainObject` type guard instead of extending the `global.object`
2. Getting `isPerson` back to an actual type guard

## 1. Using an `isPlainObject` type guard instead of extending the `global.object`

I avoid extending TypeScript's globals as much as I can. The following `isPlainObject` is a simple workaround that allows TypeScript to recognize that properties checked through `hasOwn`/`hasOwnProperty` are effectively part of the object itself but without extending `global.object`.

```ts
interface PlainObject {
  hasOwnProperty<K extends string>(key: K): this is Record<K, unknown>

  // Object.hasOwn() is intended as a replacement for Object.hasOwnProperty(). See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn
  hasOwn<K extends string>(key: K): this is Record<K, unknown>
}

function isPlainObject(value: unknown): value is PlainObject {
  return !!value && typeof value === 'object' && !Array.isArray(value)
}
```

## 2. Getting `isPerson` back to an actual type guard

Michal proposed to create a parser, then use it to create a type guard. My proposal is to leverage either the parser's perks (being a type guard that scales) and the standard type guard ones (the simplicity).

This is the result of mixing the two approaches:

```ts
function isPerson(value: unknown): value is Person {
  if (!isPlainObject(value)) return false

  if (!value.hasOwnProperty('name')) return false
  if (!value.hasOwnProperty('age')) return false

  const { name, age } = value

  if (typeof name !== 'string') return false
  if (typeof age !== 'number') return false

  // @ts-expect-error: turn off "obj is declared but never used."
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const obj: Person = { name, age }

  return true
}
```

Most of the magics come from the following lines
```ts
// @ts-expect-error: turn off "obj is declared but never used."
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const obj: Person = { name, age }
```
This is necessary because I did not like Michal's solution to create and return a **new object** every time. I am used to working on an application that heavily uses the machine's memory, and the fewer objects I create, the better. **What is the difference between creating and returning the object instead of just creating it?**
[Massimiliano Mantione](https://twitter.com/M_a_s_s_i?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor) (that worked in the V8 team, the Javascript VM inside the Chrome browser) bets that the **code related to a non-consumed variable**—a variable that is not passed to other functions and that is not returned, so it does not leave the function scope and is not shared with other scopes—**is removed at compile time** because, from a runtime perspective, it's completely useless. Hence, the object is not created at runtime, it does not pass through the garbage collector, or its nursery.

You can also play with the above example in this [TypeScript playground](https://www.typescriptlang.org/play?#code/PTAEGUBcEMCdIJYDsDmoBmsD2BbUkALAU1ACIAVATwAcjwBjWBayALlAGkijrk0raoAOIBXOABMAzhGjoS0JONABVaqHJZQAEWiQipUHET0ANiQKRI1SaxBmAbkRMjqAOhQJI9LEiRF6euKu3jjAkDREkozMkAC0ANbcvKix4bSxKGKwUrGSskSxCuKxLqlYseK6BQBMACwArADs4vXVAEbijdXVAFB9IKAAwgASAIIAckIAoqAAxACMPQOj9lgI4nz4mtAiKDhESJD4xKAoJlht0CagAPJtAFb+R8gY0KuwoFjohqDn9FcmSiGXb7Q5EJQ+Ih9ZB6WDoaD0EgABRM0GQd0eAVAAG8eqB8aACNBJDcAO5IJHYWjwSgAHg4oCIAA89IppJJIExUAA+AAUiUo7A4AEp2IQENIJaAAEr+LDZekAGlAIiQ8SQWHJ3L6BNAAwxT1cRJJ5N5wtAUphB3E4MM0mgoFgPFRiNBR3Q8tuD0NxrJFKpRBpZtcEG4hMs1lswBtjnO1NcOCwAC8ECZUa55ShgAdYspwNGsPRJMAAOpENrAABSb2gDCYLGAsrkTqQiOAQnOlxMAH0DQFi33IMBfeS8QSR0h6YyWdb2Zy+HyBULRcdLdJZd4FRxlar1ZqkNqAL59dCqgIIHwWyQotFIQe8+xXEREdi7jXkleP5wkKU39HerFcV1J1IBEWAkFAABCSCv2fUAADJ4PwCIvlAWCSAAXiw0AAHILkxSAcIQpDINGWBYGgShXAlMiKMoB8nyIYUemPJYwFiDjOK47ieN4viuLY0B+OEkThME0SJMk2JoTBOEEWRQNJEvICCSQaB9nYDkuRQMd8WgFAX1AJARBwNpA100AlP2AB+dg2iwLAzAUFj+jAEYJmmOZqhDcgTlPVtEEvKUHSdK51HAZDaEyCRQF5SQw1IdCr1AJFFJ8UhmJ6ZlqHld0z0CiCJVS2AlKQBjv1fNV3yQT9GOS4rSpxCyEG+XlIKK1F-wI8rn2Fc0QLAiD4RMeKdQJAZZQARxEBAnSUdAECcKRmtamDGKNYk-UpLBqXCXkcLU-YcL6x0iFA8DXhGqFdRa2K1u-DaTX9HbAz2nD9KIY7+rOwbLtGizvCQDkcSM9SiGVD7QEPUAMLQxixvxW7eTSIhUMOkhIOwnCtL4Y7TvOoarlGm7WpR1DIcx2GDpMszYDxgaLuG-7dX1FgLzU64ADFFpMKQMB55aWbAKZHFgIEdoKsKFqW0AcBEYGzNl6BqFoJRIG2X5CzCx8mGgNozE+JBAVAUliEKo5mQlSBJAR34zss3BDPsxyiAUUAAB8VUUIgFr8cRbaR9DHq2gMg2xx2vqa3USdisnviDqyMax52nKQemfsZonruj-FE5huGHsTizWKFoYxkmGZZgAZh8k58KeZKNSOBm-YBnxgfw9gGsvWHsVB-YIYM5U85LgkW-wWBnxcoA), where I also added an optional property to show how we could manage it.

### Updates

Please take a look at [Alexis comment](https://dev.to/sferadev/comment/1o5hc) for a little change that helps protecting against new optional properties too 😊

## Conclusions

Thanks a lot to [Michal Szorad](https://twitter.com/michalszorad), who opened my eyes with [his solution](https://levelup.gitconnected.com/typescript-keeping-type-guards-safe-and-up-to-date-2457d52bd722), [Matteo Ronchi](https://twitter.com/cef62), which helped me evolve Michal's implementation, and [Massimiliano Mantione](https://twitter.com/M_a_s_s_i?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor), that gave me the internals about the JS VM.