Zod literals with null fallback
January 20, 2024
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));