6 min read
In certain scenarios, it becomes necessary to convert object properties that are undefined to null.
This is particularly relevant in the case of Next.js, where getStaticProps
or getServerSideProps
must not return undefined properties.
Returning an undefined property results in a runtime error.
However, manually checking for possibly undefined properties is error-prone and can lead to oversights if the object changes.
The Example
Let's say we have a Calendar API that returns events of the following type:
type CalendarEvent = { summary: string; description: string | undefined; location?: string; date: { start: Date; end: Date | undefined; };};
There are three problems in the CalendarEvent
type.
- The description is a union type that can be undefined
- Location is an optional property and can be undefined as well
- The end date is a union type that is inside a nested object and can be undefined as well
Wouldn't it be nice if we could use a TypeScript helper that turns those undefined
instances into null
?
If we had such a helper, we could not forget to convert a property that is possibly undefined
.
Let's try to write such a helper.
The TypeScript Helper
First, we have to iterate over all properties of our object. This can be done with a mapped type:
type UndefinedToNull<T extends object> = { [Prop in keyof T]: T[Prop];};
First, we define a new type called UndefinedToNull
, which requires a generic T
that must be an object (extends object
).
Then, we define a new object (the curly braces) and define a property for each key of T
([Prop in keyof T]
).
For simplicity, we start by returning the same type that was passed in T[Prop]
.
Our type helper now returns the same type that is passed in:
type CalendarEventStaticProps = UndefinedToNull<CalendarEvent>;// CalendarEventStaticProps has the following shape:{ summary: string; description: string | undefined; location?: string; date: { start: Date; end: Date | undefined; }}
Not much useful, but a good starting point to modify the returned type.
Replace undefined with null
Now that we are mapping each property of our object, we can start to modify the resulting type.
We try now to map undefined
to null
.
For this, we need an additional type helper UnionUndefinedToNull
.
type UnionUndefinedToNull<T> = T extends undefined ? null : T;
This type helper gets again a generic and returns null if it is undefined; in all other cases, it returns the type as it is.
type One = UnionUndefinedToNull<undefined>;// One = nulltype Two = UnionUndefinedToNull<string>;// Two = string
It also works if we pass a union type:
type Three = UnionUndefinedToNull<string | undefined>;// Three = string | null
This is exactly what we need for our use case.
So we can add this type helper to our UndefinedToNull
type helper.
type UndefinedToNull<T extends object> = { [Prop in keyof T]: UnionUndefinedToNull<T[Prop]>};type CalendarEventStaticProps = UndefinedToNull<CalendarEvent>;// CalendarEventStaticProps has the following shape:{ summary: string; description: string | null; location?: string | null | undefined; date: { start: Date; end: Date | undefined; }}
Ok, the description looks good now, but what about location and the end date?
Optional Properties
Optional properties like location
need an extra step.
We have to remove them from the resulting type.
type UndefinedToNull<T extends object> = { [Prop in keyof T]-?: UnionUndefinedToNull<T[Prop]>;};type CalendarEventStaticProps = UndefinedToNull<CalendarEvent>;// CalendarEventStaticProps has the following shape:{ summary: string; description: string | null; location: string | null; date: { start: Date; end: Date | undefined; }}
The little -?
removes the ?
from the resulting type and with it the additional undefined
.
Now we have to fix the end date.
Recursion
The problem with the end date is that we check if the whole date object is undefined and not its properties. We have to call our type helper recursively if the property is an object.
type UndefinedToNull<T extends object> = { [Prop in keyof T]-?: T[Prop] extends object ? UndefinedToNull<T[Prop]> : UnionUndefinedToNull<T[Prop]>;};
Now we check the type of each property if it is an object (T[Prop] extends object
).
If it is an object, we call the UndefinedToNull
again with the property (UndefinedToNull<T[Prop]>
).
If it is not an object, we keep the UnionUndefinedToNull<T[Prop]>
.
With the recursive call, we get the following result:
type CalendarEventStaticProps = UndefinedToNull<CalendarEvent>;// CalendarEventStaticProps has the following shape:{ summary: string; description: string | null; location: string | null; date: { start: Date; end: Date | null; }}
Pretty nice. But what if our input type is not an object? At the moment, TypeScript yells at us, but wouldn't it be better if we could pass any type and our type helper decides what needs to happen to the type?
Input type
To allow every type as input, we only have to remove the constraint from the generic.
type UndefinedToNull<T> = { [Prop in keyof T]-?: T[Prop] extends object ? UndefinedToNull<T[Prop]> : UnionUndefinedToNull<T[Prop]>;};
If T
is an object, we map the type as described in the sections above;
if it is not an object, we pass T
directly to the UnionUndefinedToNull
.
Doing so, we can pass whatever we want.
type One = UndefinedToNull<string>;// One = stringtype Two = UndefinedToNull<undefined>;// Two = nulltype Three = UndefinedToNull<string | undefined>;// Three = string | nulltype Four = UndefinedToNull<{ name?: string }>;// Four = {name: string | null}
Now we can use our type helper in any situation.
Usage within getStaticProps
Now we can use our type helper within the getStaticProps
function.
export const getStaticProps: GetStaticProps<UndefinedToNull<CalendarEvent>> = async () => { const calendarEvent = await getCalendarEvent(); return { props: { ...calendarEvent, description: calendarEvent.description || null, location: calendarEvent.location || null, date: { ...calendarEvent.date, end: calendarEvent.date.end || null, }, }, };};
If the Calendar API changes and another property can be undefined, TypeScript will complain, and we can fix it.
Posted in: typescript, nextjs