ptpaterson
ptpaterson

Reputation: 9583

Typescript mapped type, add optional modifier conditionally

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

Answers (2)

Lauren Yim
Lauren Yim

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

kaya3
kaya3

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?: ...}.

Playground Link

Upvotes: 4

Related Questions