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.