Type Soundness in NextJS Query Parameters

16 July 2021

I was recently writing some NextJS code and came across a definition of a type that was unsound.

export default function apiHandler(request: NextApiRequest, response: NextApiResponse) {
  const { aRandomQueryParameterThatDoesntNecessarilyExist } = request.query;
  // ^ that var is of type string | string[] - typescript thinks it has a value
}

"No big deal", I thought, "I guess NextJS represent a query parameter that doesn't exist as an empty string".

export default function handler(
  request: NextApiRequest,
  response: NextApiResponse
) {
  const { fakeApiQueryParameter } = request.query;
  response.send(`value of fakeApiQueryParameter: ${JSON.stringify(
    fakeApiQueryParameter
  )}
		typeof: ${typeof fakeApiQueryParameter}
		`);
}

I got the following response:

value of fakeApiQueryParameter: undefined
typeof: undefined

Oh.

To clarify what's going on here: when you access any value on typeof NextApiRequest["query"], typescript believes that property to be string | string[], and doesn't make you do a check for undefined - even though in practice, when an API request comes in without a query parameter, the value is undefined. If you then try to access a property, for example length (because that exists on both string and string[], but not on undefined), an error will be thrown at runtime, and typescript won't catch that error at compile time.

Unless!

Typescript 4.1 introduced the noUncheckedIndexedAccess compiler option, which in effect adds | undefined to every index signature, so that typescript will expect you to check for undefined. In the case above, it will fail at compile time - hooray, no uncaught errors at runtime!

In very happy news, this applies to types outside of your own codebase - so there's no need to submit a PR to NextJS asking them to turn this option on (or adding undefined to their type definition).

I think that noUncheckedIndexedAccess is a great option and should be turned on. I have done so in my codebase, and was delighted to catch several other areas where runtime errors could have happened. I don't think there are significant downsides - given the nature of index signatures (specifically that they can be indexed by any string or any number), it's unlikely that they will ever be comprehensive.

Of course, I can imagine a scenario where this check adds more boilerplate code than you might want. If you know that your endpoint is only going to be called in a specific way, and you're happy with an uncaught exception at runtime if your endpoint is called in some other way, maybe you'd prefer to keep it turned off.

Bonus: if you're sure a value at a given key has been assigned, you can override typescript with !.

type T = {
    [key: string]: string
}

const key: string = "aKeyThatExists"

const myObject: T = {
    [key]: "a value"
}

typeof myObject[key] !== 'undefined'
  // use ! to tell typescript that here you know the value is defined
  // because you know that the key previously was assigned a value in the object
  ? myObject[key]!
  : "fallback value";

Have I missed something? Is there a context in which the burden of checking the existence of every index signature before access outweighs the benefit of doing away with this type of uncaught runtime crash? Let me know on Twitter or Mastodon! I would love to hear your experience.