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

data type before

exactOptionalPropertyTypes: true

data type after

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');
}

References

tsconfig#exactOptionalPropertyTypes