Typescript DeepPick partial properties as a way to handle default options

39 Views Asked by At

I want to pick all the optional properties of a nested object type.

Example:

// Options that the user passes. He must use 'a.b.mandatory'.
type Options = {
  a: {
    b: {
      mandatory: number;
      c?: {
        d?: string;
        e: string;
        f?: string;
      }
    }
  }
};

// The default options are for the non-mandatory parts of 'Options'. 
// If the user doesn't provide 'c' then I want to provide default options for it. 
// See that 'e' is mandatory like in Options because when providing 'c', 
// 'e' must be provided as well.
type DefaultOptions = BlackBox<Options>;

// -->
// {
//   a: {
//     b: {
//       c: {
//         d?: string;
//         e: string;
//         f?: string;
//       }
//     }
//   }
// };

Then, I can set my default options like so:

const defaultOptions: DefaultOptions = {
  a: {
    b: {
      c: {
        d: 'default value',
        e: 'mandatory-default value'
      }
    }
  }
};

This is for later merge using a tool like [defu][1] to get the final options the user can safely work with:

function f(options: Options) {
  const optionsWithDefaults = defu(options, defaultOptions);
}

I want everything to be generically typed.


Edit: Added clarification with an example

type LambdaOptions = {
  aws: {
    lambda: {
      name: string;
      version: string;
    };
  };
  middy?: {
    middlewares?: MiddlewareObject[];
  };
  sentry: {
    dsn: string;
    tracesSampleRate?: number;
    rethrowAfterCapture?: boolean;
  };
};

const defaultOptions: DefaultOptions<LambdaOptions> = {
  sentry: {
    tracesSampleRate: 0.1
  }
};

const options: Options = {...};
const optionsWithDefaults = defu(options, defaultOptions);
// -->
// {
//   aws: {
//     lambda: {
//       name: string;
//       version: string;
//     };
//   };
//   middy?: {
//     middlewares?: MiddlewareObject[];
//   };
//   sentry: {
//     dsn: string;
//     tracesSampleRate: number;
//     rethrowAfterCapture?: number;
//   };
// };

// sentry.tracesSampleRate is protected by a default value


  [1]: https://github.com/unjs/defu
1

There are 1 best solutions below

0
Aderion On

I've implemented something similar a while ago.

For a nice type to help with declaring your default config:

type Primitive = undefined | null | boolean | string | number;
// Deeply make all fields optional
type DeepOptional<T> =
  T extends Primitive | any[] ? T // If primitive or array then return type
  : {[P in keyof T]?: DeepOptional<T[P]>} // Make this key optional and recurse

Then you can define your default config with intellisense like:

const a: DeepOptional<Config>={...}

For the resulting type to be correct:

What you want to achieve can be done with a merge function returning a intersection of the types like.

function merge<A extends Object, B extends Object>(a:A,b:B): A & B

Since the result is A & B, B will overwrite A's optional statement if the same key is given without it.

You can check this out for a working example: https://github.com/Aderinom/typedconf/blob/master/src/config.builder.ts#L118


Edit: Changed incorrect wording (union to intersection) based on jcalz comment