How to assert generic is NonNullable

Viewed 276

I have some methods that use NonNullable<T> to allow certain parts to assume that null checks have been performed before being called. I also have a function that will perform that check before then calling aforementioned methods.

My problem is that this function accepts a generic T which I need to narrow down to NonNullable<T>. When writing guarding code that checks for null and undefined values, this type is not automatically narrowed down to a non-nullable type by means of inference.

export interface UrlParam<T> {
  defaultValue: T,
  encode: (value: NonNullable<T>) => string
  decode: (value: string) => NonNullable<T>
}

interface LatLng {
  lat: number
  lng: number
}

const COORDINATE_DELIMITER = ','

const locationParam: UrlParam<LatLng | null> = {
  defaultValue: null,
  encode: ({ lat, lng }) => [lat, lng].join(COORDINATE_DELIMITER),
  decode: value => {
    const [lat, lng] = value.split(COORDINATE_DELIMITER)

    return {
      lat: Number(lat),
      lng: Number(lng),
    }
  }
}

function encodeParam<T>(urlParam: UrlParam<T>, value: T) {
  return value !== undefined && value !== null ? urlParam.encode(value) : null
}

The encodeParam function produces the following compilation error:

Argument of type 'T' is not assignable to parameter of type 'NonNullable'

I can resolve this by forcing the TypeScript compiler to treat T as NonNullable<T>:

urlParam.encode(value as NonNullable<T>)

However I am looking for a solution where the compiler can actually infer this type based on the condition itself. Is this possible? Am I perhaps approaching this the wrong way?

1 Answers

While it might one day be possible to do this sort of thing with control flow analysis and not a type assertion as in value as NonNullable<T> or the terser value! using the non-null assertion operator, for now the compiler will just not do this by itself.


Instead, if you want something the compiler can follow, my suggestion here would be to change T from meaning "the possibly-null or undefined type from which I need to remove null and undefined" to meaning "the non-null and non-undefined type to which I may need to add null and undefined. You can use the empty type {} to represent "anything except null and undefined" (yes, even primitives like string are assignable to {}), and constrain T extends {} so that no undefined or null types can sneak into your T type.

The refactoring would look like this:

export interface UrlParam<T extends {}> {
  defaultValue: T | null | undefined,
  encode: (value: T) => string
  decode: (value: string) => T
}

and then instead of UrlParam<LatLng | null> you could write UrlParam<LatLng>, and finally the following encodeParam will work as expected, because the compiler can use control flow analysis to filter a value of type T | null | undefined to just T by checking it:

function encodeParam<T extends {}>(urlParam: UrlParam<T>, value: T) {
  return value !== undefined && value !== null ? urlParam.encode(value) : null // okay
}

Playground link to code

Related