Search code examples
typescriptmapped-types

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.


Solution

  • 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