In TypeScript, a property is considered optional if it can be omitted from an object, meaning it can be either undefined
or not provided at all. Optional properties are denoted using the ?
suffix on the property key. Determining whether a property is optional or explicitly defined with undefined
as its type can be quite tricky.
Let's consider the following example with five possible combinations:
type Example = {
required: number;
optional?: number;
requiredAsUndefined: undefined;
requiredWithUndefined: number | undefined;
optionalWithUndefined?: number | undefined;
}
The last four properties are allowed to be undefined
, but only the second and fifth are actually optional. Interestingly, the properties optional
, requiredWithUndefined
, and optionalWithUndefined
all resolve to the same union type number | undefined
.
So, what we want is a type that returns true
for optional
and optionalWithUndefined
, and false
for the rest. Here's how such a utility type can look:
type IsOptional<T, K extends keyof T> = undefined extends T[K]
? ({} extends Pick<T, K> ? true : false)
: false;
type Required = IsOptional<Example, 'required'>; // false
type Optional = IsOptional<Example, 'optional'>; // true
type RequiredAsUndefined = IsOptional<Example, 'requiredAsUndefined'>; // false
type RequiredWithUndefined = IsOptional<Example, 'requiredWithUndefined'>; // false
type OptionalWithUndefined = IsOptional<Example, 'optionalWithUndefined'>; // true
There are two constraints in this utility type. The first constraint, undefined extends T[K]
, checks if undefined
can be a part of the type accessed by T[K]
. It essentially asks whether the type T[K]
can include undefined
. The second constraint, {} extends Pick<T, K> ? true : false
, ensures that the type {}
(an empty object) is assignable to a type where the property K
is picked, implying the property is optional.
From this utility type, we can build a new mapped type which only picks optional properties. The non-optional properties will be set to never
:
type OptionalProperties<T> = {
[K in keyof T]: IsOptional<T, K> extends true
? T[K]
: never
}
type OnlyOptionals = OptionalProperties<Example>;
// type OnlyOptionals = {
// required: never;
// optional?: number;
// requiredAsUndefined: never;
// requiredWithUndefined: never;
// optionalWithUndefined?: number | undefined;
// }
Having the properties with type never
is usually enough for type safety, but in case we truly want to omit these properties for stylistic purposes, we can move the IsOptional<T, K> extends true
constraint into the square brackets. This is a nice little trick because it will set the key of non-optional properties to never
, which will then get omitted by TypeScript automatically.
type OnlyOptionals<T> = {
[K in keyof T as IsOptional<T, K> extends true ? K : never]: T[K]
}
type OnlyOptionals = OnlyOptionals<Example>;
// type OnlyOptionals = {
// optional?: number;
// optionalWithUndefined?: number | undefined;
// }
Here's the Playground to try it out directly in the browser: TypeScript Playground
Top comments (0)