removing falsy keys

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.

Handling The Any Type

How might we solve this shortcoming of the initial solution?

A few possibilities include:

  1. Cloning the original object and then mutating the new version
  2. Handling “unsafeKeys”

Cloning Approach

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.

Unsafe Keys

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
}

Wrap Up

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!