Aseem Kishore
Aseem Kishore

Reputation: 10858

Is there a way to define a required type for just optional properties/defaults?

I'm specifying a type for a function's input. I want to make some properties required and others optional:

type Input = {
  // Required:
  url: string,
  method: string,

  // Optional:
  timeoutMS?: number,
  maxRedirects?: number,
}

I also want to define a defaults object internally, which I can copy/merge into inputs to "fill in" any omitted optional properties.

I'd like to strongly type this defaults object so that:

  1. I can enforce that I haven't forgotten any optional property.
  2. I don't need to specify any required property. (Ideally, I'd love to even enforce that I can't specify any required property.)
const defaults: WhatShouldThisTypeBe = {
  timeoutMS: 30000,
  maxRedirects: 5,
}

I know that Required<Input> achieves #1, but fails #2, while Partial<Input> achieves #2 (tho not the ideal #2) , but fails #1. I haven't been able to figure out how to combine those, e.g. with & or |, or use other utility types to achieve both goals.

Here's a playground link if helpful. Thank you!

Upvotes: 1

Views: 1041

Answers (1)

jcalz
jcalz

Reputation: 327624

The most important piece you need is to identify the optional properties of an object type programmatically. Here's one way to do it (as long as your object type doesn't have index signatures):

type OptionalProperties<T> = {
  [K in keyof T]-?: Partial<Pick<T, K>> extends Pick<T, K> ? K : never
}[keyof T]

Because a type with an optional property like {a?: string} does not extend the same type with a required property like {a: string}, we can check to see whether an optional version of each property K does or does not extend the actual property. We use the Pick<T, K> utility type to focus on just the part of the object with the key K. If the optional version (using the Partial<T> utility type) does extend the actual version, then the property K is optional and we keep it, otherwise we throw it away as never. For your Input type, this evaluates to:

type OptionalInputProps = OptionalProperties<Input>
// type OptionalInputProps = "timeoutMS" | "maxRedirects"

From there we can just use the Required<T> utility type and Pick to grab just that part of Input and make its properties required:

type WhatShouldThisTypeBe = Required<Pick<Input, OptionalProperties<Input>>>;
/* type WhatShouldThisTypeBe = {
    timeoutMS: number;
    maxRedirects: number;
} */

And then things work as desired:

const missingDefaults: WhatShouldThisTypeBe = { // error!
  //  ~~~~~~~~~~~~~~~
  //  Property 'maxRedirects' is missing
  timeoutMS: 30000,
};

const extraDefaults: WhatShouldThisTypeBe = {
  method: 'GET', // error!
  //~~~~~~~~~~~ <--
  //Object literal may only specify known properties
  timeoutMS: 30000,
  maxRedirects: 5,
}

Notice that WhatShouldThisTypeBe does not expressly forbid the required properties like method, but if you assign an object literal directly to a variable of that type, the compiler will catch it with excess property checking. This is not foolproof; you can get around it by doing the assignment indirectly:

const unannotatedWithExtras = {
  method: 'GET',
  timeoutMS: 30000,
  maxRedirects: 5,
}
const uncaughtExtraDefaults: WhatShouldThisTypeBe =
  unannotatedWithExtras; // okay, no error

TypeScript works this way intentionally; see this question and its answer for more info.


Playground link to code

Upvotes: 2

Related Questions