Reputation: 10858
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:
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
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.
Upvotes: 2