Reputation: 9583
Is it possible to make a mapped type property optional conditionally?
Consider this type
type Definition {
name: string,
defaultImplementation?: ImplementationType
}
and a record of them:
type DefinitionMap = Record<string, Definition>
I would like to make a mapped type that has an implementation that is optional if the input is provided, but the mapped type implementation required if it wasn't.
For an DefinitionMap
like this
{
foo: { name: 'x' },
bar: { name: 'y', defaultImplementation: { /*...*/ } }
}
I would like to have a mapped type like
{
foo: ImplementationType,
bar?: ImplementationType
}
I've been trying to use conditionals and add undefined
to the type, but that is not working.
type ImplementationMap<T extends DefinitionMap> = {
[K in keyof T]: T[K] extends { defaultImplementation: any }
? ImplementationType | undefined
: ImplementationType
}
I know that the conditional branches behave how I want them to, but adding undefined
doesn't actually make the field optional.
Upvotes: 6
Views: 2340
Reputation: 14088
I'm assuming DefinitionMap
should be Record<string, Definition>
(instead of Record<string, A>
).
Try this:
// Gets the keys of T whose values are assignable to V
type KeysMatching<T, V> = {[K in keyof T]: T[K] extends V ? K : never}[keyof T]
type ImplementationMap<T extends DefinitionMap> =
// A partial (all properties are optional) record for all the keys
Partial<Record<keyof T, ImplementationType>> &
// Require ImplementationType for all the keys that do not have defaultImplementation
Record<KeysMatching<T, { defaultImplementation?: undefined }>, ImplementationType>
/*
Test is equivalent to
{
foo: ImplementationType,
bar?: ImplementationType,
baz: ImplementationType
}
*/
type Test = ImplementationMap<{
foo: { name: 'x' },
bar: { name: 'y', defaultImplementation: { /*...*/ } },
baz: { name: 'z', defaultImplementaiton: undefined }
}>
Upvotes: 7
Reputation: 51034
Here's a solution:
type NonImplementedKeys<T extends DefinitionMap> = {[K in keyof T]: T[K] extends {defaultImplementation: ImplementationType} ? never : K}[keyof T]
type NiceIntersection<S, T> = {[K in keyof (S & T)]: (S & T)[K]}
type ImplementationMap<T extends DefinitionMap> = NiceIntersection<{
[K in NonImplementedKeys<T>]: ImplementationType
}, {
[K in keyof T]?: ImplementationType
}>
Example:
type DefinitionMapExample = {
foo: { name: 'x' },
bar: { name: 'y', defaultImplementation: { /*...*/ } }
}
// {foo: ImplementationType, bar?: ImplementationType | undefined}
type ImplementationMapExample = ImplementationMap<DefinitionMapExample>
The NiceIntersection<S, T>
type is equivalent to a plain intersection type S & T
, except it makes the result look like {foo: ..., bar?: ...}
instead of {foo: ...} & {bar?: ...}
.
Upvotes: 4