In case you come across an object that embodies both a User
and a Book
while also serving as a UserId
, using an intersection is not advisable. Instead, the appropriate data type to use would be the union of these types:
type DataRow = User | Book | UserUID;
It's worth noting that attempting to access a key in a union-typed object that only exists in certain members of the union will lead to complications. Object types such as interfaces are open and can include properties unknown to the compiler; for instance, the author
property of a User
may not necessarily be missing but could hold any value.
There are two potential ways to handle this type of situation.
One workaround to avoid unexpected any
properties entirely involves using a type like ExclusifyUnion
, which is elaborated in the answer provided here. This method entails explicitly indicating any unanticipated properties from other union members as missing in each union member, translating to them being undefined
if accessed:
type DataRow = ExclusifyUnion<User | Book | UserUID>;
The revised structure ensures that every union member clearly defines each property key, with many marked as optional-and-undefined
.
With this type in place, most of your code should function smoothly, though you may encounter undefined
types for columns not present in all models.
const columnDefinitions: ColumnDefinitionMap = {
// Add mappings here
};
While the value
parameter within id
's valueFormatter
maintains the type string | number
, the equivalent for admin
reflects boolean | undefined
, anticipating scenarios where you process the admin
field of a Book
. To rectify this, consider altering the type of value
from DataRow[K]
to
Exclude<DataRow[K], undefined>
utilizing
the Exclude
utility type.
The alternative approach involves retaining the original union but leveraging type functions to represent 'a key from any member of the union' and 'the resultant type when indexing into an object of a union type with a key, disregarding unidentified union members':
type AllKeys<T> = T extends any ? keyof T : never;
type Idx<T, K> = T extends any ? K extends keyof T ? T[K] : never : never;
These functions act on unions by processing individual members following distributional conditional logic.
The resultant types look like this:
export interface ColumnDefinition<K extends AllKeys<DataRow>> {
// Define properties here
}
type ColumnDefinitionMap = {
[K in AllKeys<DataRow>]?: ColumnDefinition<K>;
};
Subsequently, your code should operate as anticipated:
const columnDefinitions: ColumnDefinitionMap = {
// Include definitions
};
These strategies cater to different requirements, yet regardless of the chosen method, it is essential to work with a union rather than an intersection.
Link to Playground containing code