DEV Community

Cover image for Keeping TypeScript Type Guards safe and up to date (a simpler solution)
Stefano Magni
Stefano Magni

Posted on • Updated on

Keeping TypeScript Type Guards safe and up to date (a simpler solution)

This article is about improving the solution shared by Michal Szorad in his TypeScript: Keeping Type Guards Safe and Up To Date article.

Photo by Brian McGowan on Unsplash


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

interface Person {
  name: string
  age: number
}
Enter fullscreen mode Exit fullscreen mode

and this is a type guard for the above Person

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'
  )
}
Enter fullscreen mode Exit fullscreen mode

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

interface Person {
  name: string
  age: number
  something: string // <-- a new property
}
Enter fullscreen mode Exit fullscreen mode

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

Michal Szorad already discussed this problem in his TypeScript: Keeping Type Guards Safe and Up To Date 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.

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)
}
Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

Most of the magics come from the following lines

// @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 }
Enter fullscreen mode Exit fullscreen mode

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 (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, where I also added an optional property to show how we could manage it.

Updates

Please take a look at Alexis comment for a little change that helps protecting against new optional properties too 😊

Conclusions

Thanks a lot to Michal Szorad, who opened my eyes with his solution, Matteo Ronchi, which helped me evolve Michal's implementation, and Massimiliano Mantione, that gave me the internals about the JS VM.

Top comments (4)

Collapse
 
cohlar profile image
cohlar

Very cool, thanks for sharing! I was looking for exactly that and intend to adapt it to assertion functions.

Just a quick note - I'm not sure why those complicated hasOwnProperty and hasOwn types are needed, and I simplified as follows.
Would love to hear from you @noriste @alexis what could go wrong with my code.

Collapse
 
noriste profile image
Stefano Magni

Uhm, I think you are right, we do not need them...

Collapse
 
sferadev profile image
Alexis Rico

I really like the approach, however it doesn't alert you if you add new optional properties.

Suggestion to make it more robust: typescriptlang.org/play?#code/PTAE...

Collapse
 
noriste profile image
Stefano Magni

I just updated the article pointing to your suggeste change, thank you!! 😊