2021-10-01
|~3 min read
|448 words
I was working on a project recently where I was taking data received from an API call and enriching it.
Not all data was defined in all cases. This was expected. It was also expected, per our type definition, that if the data’s missing, the key would be absent.
To make this more concrete, imagine the following:
type Sandwich = {
toppings: string[]
bread: "sourdough" | "rye" | "white" | "whole grain"
free?: true
}
So, while the following meets the type definition:
const rueben: Sandwich = {
toppings: ["corned beef", "Swiss cheese", "sauerkraut", "Russian dressing"],
bread: "rye",
free: undefined,
}
I wanted it to be:
const rueben: Sandwich = {
toppings: ["corned beef", "Swiss cheese", "sauerkraut", "Russian dressing"],
bread: "rye",
}
One way to do that is the following:
export const removeFalsy = (generatedObject: Record<string, unknown>) => {
Object.keys(generatedObject).forEach((key) => {
if (!generatedObject[key]) {
delete generatedObject[key]
}
})
return generatedObject
}
This would then be used like:
const slimReuben = removeFalsy(reuben)
This produces the correct result, however the type of slimReuben
isn’t Sandwich
, but Record<string, unknown>
.
One way to accomplish the task is with a generic that extends the record:
function removeFalsy<T extends Record<string, unknown>>(obj: T): T {
const newVersion: any = {}
Object.keys(obj).forEach((key) => {
if (obj[key]) {
newVersion[key] = obj[key]
}
})
return newVersion
}
The problem here is that newVersion
is typed as any
.
How might we solve this shortcoming of the initial solution?
A few possibilities include:
The first approach might look like this:
function removeFalsy<T extends Record<string, unknown>>(obj: T): Partial<T> {
const newVersion: Partial<T> = { ...obj } Object.keys(newVersion).forEach((key) => {
if (!newVersion[key]) {
delete newVersion[key] }
})
return newVersion
}
This totally works! We create a copy of the object so when we modify it with the delete
we aren’t changing the original Reuben
as well.
I don’t love it because it means creating a full copy and then removing - which feels unnecessary.
Another option is using a helper function which I’ll call unsafeKeys
:
const unsafeKeys = Object.keys as <T>(obj: T) => Array<keyof T>
function removeFalsy<T extends Record<string, unknown>>(obj: T): Partial<T> {
const newVersion: Partial<T> = {}
unsafeKeys(obj).forEach((key) => {
if (obj[key]) {
newVersion[key] = obj[key]
}
})
return newVersion
}
At the end of the day, I have a number of solutions and I’ve learned a bunch about Typescript along the way.
A lot of this is also available in an interactive playground.
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!