Life at Ataccama
Published in

Life at Ataccama

How to Convert Object Props With Undefined Type to Optional Properties in TypeScript

As a front-end developer, I solve problems every day. At Ataccama we even call our more difficult problems “challenging fun” to stay motivated when working against a big deadline or tackling something that we don’t know how to solve right away.

A while ago, I was faced with an interesting TypeScript problem. I was creating a configuration object for a table with multiple columns. Each column configuration has a fixed set of properties. Some of those properties can have undefined as a valid type. This makes them effectively optional properties, so as a lazy developer I don't want to add those properties to the config to save some typing.

But when I tried to do so TypeScript complained. TypeScript was treating those properties as mandatory, even if their value is undefined.

type ColumnConfig = {   
name: string|undefined
enabled: boolean
empty: boolean
dataPath: string[]
renderer: ((props: {rowData: any, cellData: any}) =>
React.ReactNode) | undefined
size: string|undefined
}
// This will not work, TS forces me to type in even the optional
// props. A lot of unnecessary typing.
const configNok: ColumnConfig = {
enabled: true,
empty: false,
dataPath: []
}
// TS Error: Type '{ enabled: true; empty: false; dataPath: never[]; // }' is missing the following properties from type 'ListingConfig':
// name, renderer, size

So I had to convert the given ColumnConfig type into the new type where all the properties with undefined type are converted to optional properties.

// I want ColumnConfig type...
type ColumnConfig = {
name: string|undefined
enabled: boolean
empty: boolean
dataPath: string[]
renderer: ((props: {rowData: any, cellData: any}) =>
React.ReactNode)|undefined
size: string|undefined
}
// convert to this:
type ColumnConfig = {
name?: string
enabled: boolean
empty: boolean
dataPath: string[]
renderer?: ((props: {rowData: any, cellData: any}) =>
React.ReactNode)
size?: string
}

The question is, how can we do it in TypeScript? I was eager to solve this puzzle. Also, I took this as a great opportunity to sharpen my TS skills.

You might be wondering why don’t I change the original type directly?

Our product Ataccama ONE is used for data governance and data quality monitoring. It is built in a modular and generic way to handle and display data of any type and shape our customers might have. For that reason, we have built our own runtime validation library to ensure all the components get proper data and the app doesn’t break. The ColumnConfig type is inferred from a runtime validation function. I can’t change this type directly. Instead, I have to transform the returned type.

TLDR

Here is the entire solution for those who are impatient or have the same problem and don’t want to scroll all the way to find the answer. If you are interested in a full explanation of what it does and how I got there, please continue reading.

type GetMandatoryKeys<T> = {
[P in keyof T]: T[P] extends Exclude<T[P], undefined> ? P : never
}[keyof T]
type MandatoryProps =
Pick<ColumnConfig, GetMandatoryKeys<ColumnConfig>>
type ConfigWithOptionalProps = Partial<ColumnConfig> & MandatoryProps

If you want an interactive example, head over to the TypeScript REPL.

Long version

First I thought about mapped types as we can add or remove optional modifiers in mapped types. Sadly, this won’t work since we can’t do so conditionally. So I turned to Google and after a little bit of searching I found an interesting question on StackOverflow: TypeScript mapped type, add optional modifier conditionally. That seemed a lot like what I was looking for.

While not exactly what I wanted to solve, it gave me a push in the right direction. I found out I can use Partial type to make all the keys optional and then use type intersection to make some of the keys mandatory again. It’s a nice trick I didn't think of earlier.

All that was left was to figure out how to get the mandatory values. My first take on this was simple mapped type:

type GetMandatoryKeys<T> = {
[K in keyof T]: T[K] extends undefined ? K : never
}[keyof T]

While it seemed valid at first I soon discovered that the condition will evaluate to true only when the object prop is exactly of undefined type. But a union of several types will fail and TS will evaluate it to never type.

type Obj = {name: string | number | undefined}type Keys = GetMandatoryKeys<Obj>
// will return never
// In TS type `string | number | undefined` doesn't extend `undefined`
// in mapped types.

So I had to perform the extends condition check on the entire union type somehow. But those unions are dynamic, so I can't simply include all variants.

Then, the eureka moment came. I realized I can use Exclude type to remove certain types from the union. For example, we can easily remove undefined from string | number | undefined using Exclude<string | number | undefined, undefined>. And if the union type doesn't include undefined the type will come out unaltered.

So here is the new version of GetMandatoryKeys using Exclude and it works like a charm:

type GetMandatoryKeys<T> = {
[P in keyof T]: T[P] extends Exclude<T[P], undefined> ? P : never
}[keyof T]

This type will map over the object props and check if their type contains undefined (by comparing the original prop type with the prop type without undefined type). If yes, it will return never type, effectively removing the key. In other cases, it will return the key of a given property.

And that’s it! Once we have mandatory keys we can easily extract mandatory properties from the object type using Pick utility type and combine it with Partial type to have our optional types truly optional. And TypeScript is happy as well.

type ColumnConfig = {
name: string|undefined
enabled: boolean
empty: boolean
dataPath: string[]
renderer: ((props: {rowData: any, cellData: any}) => React.ReactNode)|undefined
size: string|undefined
}
type GetMandatoryKeys<T> = {
[P in keyof T]: T[P] extends Exclude<T[P], undefined> ? P : never
}[keyof T]
type MandatoryProps = Pick<ColumnConfig, GetMandatoryKeys<ColumnConfig>>type ConfigWithOptionalProps = Partial<ColumnConfig> & MandatoryProps// TS see name, renderer, size as optional props and doesn't complain 👌
const configOk: ConfigWithOptionalProps = {
enabled: true,
empty: false,
dataPath: []
}

Hope this will help you as well, and happy coding!

Want to solve problems like this with Tomas? Join his team! See our open positions here.

--

--

--

We are a growing, international software company developing an AI-powered platform to help our customers process, manage, and monitor (big) data. We like #ChallengingFun. Do you?

Recommended from Medium

Typed errors with discriminated unions

Sentry Pro Tips

How to achieve a dark mode theme in JavaScript with just a few lines of JavaScript code

How to find memory leaks in JavaScript

An introduction to WebAssembly

Setting up complete React Redux SPA application

Getting Active: the Beginning

Confusing LeetCode

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tomas Pustelnik

Tomas Pustelnik

On a mission to build a better web • I write about HTML/CSS, accessibility, and web perf • Dev productivity and tooling geek • Doing FE at @ataccama

More from Medium

Setting Up ESLint with React/TypeScript

Typescript: A guide for faster onboarding process

What’s and why’s of state in React.

Testing a HTTP Error Handler Utility Hook