Engineering

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

Tomas Pustelnik

5 minutes read

Sat Aug 27, 2022

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  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 .

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  type into the new type where all the properties with  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  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  only when the object prop is exactly of  type. But a union of several types will fail and TS will evaluate it to  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  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  type to remove certain types from the union. For example, we can easily remove  from  using . And if the union type doesn't include  the type will come out unaltered.

So here is the new version of  using  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  (by comparing the original prop type with the prop type without  type). If yes, it will return  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  utility type and combine it with  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.