typescripttypescript4.0

TypeScript null check for parent object of a mutable location


I have a React app using TypeScript 4.8.4.

import { isNil } from "lodash";  // isNil(value: any): value is null | undefined

const order = ...  // { uuid: string } | null

const { data } = useOrderQuery(
  isNil(order?.uuid)
    ? { skip: true }
    : { variables: { uuid: order.uuid } },  // variables requires { uuid: string }
);

If !isNil(order?.uuid), order.uuid is a string, which means that order must not be null (if it was null, the optional chaining would produce undefined for order.uuid).

However, tsc flags the order.uuid access on the last line, saying error TS2531: Object is possibly 'null'.

I think this is because order.uuid is a mutable location (I may be wrong). If that's the case, why does TypeScript think it's possible for the type to change in between the ternary condition and the false branch of the ternary?

There are lots of changes to the condition that fix this (isNil(order) || isNil(order.uuid), test a new orderUuid variable, order?.uuid === null || order?.uuid === undefined, etc), but they're all more verbose.

VSCode doesn't complain (I think it's using the latest TypeScript version).

Upgrading my TypeScript version fixes this (this is fixed somewhere between 5.2.1 and 5.3.3), but I'd love to find a fix that works for my current version, or at least understand the issue better.


Solution

  • You're encountering microsoft/TypeScript#35974 in which the effects of a custom type guard function did not propagate up through an optional chaining expression. It isn't that TypeScript thought it was "possible" for the narrowed value to get re-widened in the middle of your expression; it's that TypeScript didn't really perform the narrowing you were expecting at all. Such narrowings don't happen automatically in TypeScript merely by logical implication; they need to be explicitly implemented in the compiler.

    And this narrowing was, indeed, implemented in microsoft/TypeScript#55613, which was released with TypeScript 5.3. So the recommended way to deal with this is to keep your version of TypeScript up to date. This isn't always easy, but the language moves fast enough that dependencies on a particular version of the language tends to accumulate technical debt quickly. If you're stuck on TypeScript 5.2 or below, then you'll need to modify your code in some way. The easiest approach is to just use a non-null assertion (!) like { variables: { uuid: order!.uuid } } and move on.

    Playground link to code