- Published on
Handling Optional Properties in TypeScript
Overview
⚠ This post is based on TS v4.4.4.
Sample codes
Problem: Optional and undefined
Sometimes we want to ensure that all the props of a record are not nullish.
Here's a simple type guard implementation:
type NonNullish<T> = T extends undefined | null ? never : T;
function isNotNullish<T>(x: T): x is NonNullish<T> {
return x !== null && x !== undefined;
}
type Must<T> = {
[K in keyof T]: NonNullish<T[K]>;
};
/**
* Type guard that checks all the props are non-nullish.
*/
function isMust<T extends Record<string, unknown>>(x: T): x is Must<T> {
return Object.keys(x).every((key) => isNotNullish(x[key]));
}
Now we can use the type guard like this:
// data.name is nullish.
const data: { name: string | null; price?: number } = {
name: 'apple',
};
if (isMust(data)) {
// data.name is not nullish here.
expectType<{ name: string; price?: number }>()(data);
console.log(data.name.charAt(0));
}
Looks good. But this type guard has a caveat:
const data: { name: string | null; price?: number } = {
name: 'apple',
// price is optional so we can assign undefined to it.
price: undefined,
};
// isMust(data) returns false since data.price is nullish.
if (isMust(data)) {
// Cannot reach here
expectType<{ name: string; price?: number }>()(data);
console.log(data.name.charAt(0));
}
It doesn't work well with optional properties.
An optional property in TS can be undefined or absent.
So, we can assign undefined to the optioanl properties.
This type information is valid in general, but when we handle keys of an object, it can break type safety.
Solution: Exact optional property types
To solve the problem, we should differentiate the absence of the property from the property defined as undefined
.
Fortunately, TS offers an option for us: exactOptionalPropertyTypes
.
// exactOptionalPropertyTypes: true
const data: { name: string | null; price?: number } = {
name: 'apple',
// Cannot assign undefined to the optional property.
// @ts-expect-error: Type 'undefined' is not assignable to type 'number'.
price: undefined,
};
Here's how the data type is inferred.
⬇ exactOptionalPropertyTypes: false
⬇ exactOptionalPropertyTypes: true
With this option, we can use the type guard safely.
const data: { name: string | null; price?: number } = {
name: 'apple',
// Cannot assign undefined to the optional property.
// So, we can safely narrow the object type to ensure that every property is not nullish, except for optional properties.
// price: undefined,
};
// isMust(data) => true
if (isMust(data)) {
expectType<{ name: string; price?: number }>()(data);
console.log(data.name.charAt(0));
} else {
throw new Error('Will not reach here');
}