Musings of a Programmer

Rarely-used blog of Dan Harper.
View all blog posts

Working with an external API which returns a string union which may grow over time, I wanted to maintain type-safety in my code. When that API does return an unexpected value, I needed to refine it to null so I can better handle it.

I initially tried just passing them through as a plain string:

z.union([
  z.literal('foo'),
  z.literal('bar'),
  z.literal('baz'),
  z.string(),
]).nullish();

But the resulting TypeScript type is just string, which is not what I want. Took me a bit of digging through the Zod docs, but I arrived at using transform:

const KNOWN_VALUES = ['foo', 'bar', 'baz'] as const;

z.string()
  .nullish()
  .transform((val) =>
    KNOWN_VALUES.includes(x as any)
      ? (x as (typeof KNOWN_VALUES)[number])
      : null,
  );

The resulting type is now null | 'foo' | 'bar' | 'baz' - nice!

That's a bit tedious to write, so extracted into a helper function, we have:

export function oneOfOrNull<T>(arr: readonly T[]) {
  return (val: unknown) => {
    return arr.includes(val as any) ? (val as T) : null;
  };
}

Which we can use like so:

z.string()
  .nullish()
  .transform(oneOfOrNull(['foo', 'bar', 'baz'] as const));