DEV Community

Cover image for TypeScript Optional vs. Nullable: Model API Fields Correctly
Muhammad Azlaan Zubair
Muhammad Azlaan Zubair

Posted on Originally published at letinix.com

TypeScript Optional vs. Nullable: Model API Fields Correctly

In TypeScript, an optional property and a nullable property describe different API states. name?: string means the property may be absent; name: string | null means the property is required but its value may be null. Use name?: string | null only when both conditions are allowed by the contract.

This distinction matters when you turn JSON into types: one sample can show a value of null, but it cannot prove whether the property may be omitted in other responses. If you have a sample ready, the Letinix JSON to TypeScript Converter can generate a starting declaration; review the result against the actual API contract.

The three shapes

Consider these illustrative API response shapes:

present.json

{ "displayName": "Mina" }
Enter fullscreen mode Exit fullscreen mode

null.json

{ "displayName": null }
Enter fullscreen mode Exit fullscreen mode

missing.json

{}
Enter fullscreen mode Exit fullscreen mode

They are not interchangeable. The first contains a string value. The second contains the property with a JSON null value. The third does not contain the property at all.

Here are the corresponding TypeScript models:

user.ts

interface UserWithName {
  displayName: string;
}

interface UserWithNullableName {
  displayName: string | null;
}

interface UserWithOptionalName {
  displayName?: string;
}

interface UserWithOptionalNullableName {
  displayName?: string | null;
}
Enter fullscreen mode Exit fullscreen mode

Choose the narrowest model that matches the documented contract. Making every field optional may quiet compiler errors, but it also weakens the guarantee your type provides.

What ? means

The ? modifier makes a property optional. An object typed with displayName?: string may omit that key:

usage.ts

const a: UserWithOptionalName = {};
const b: UserWithOptionalName = { displayName: "Mina" };
Enter fullscreen mode Exit fullscreen mode

When you read the property, TypeScript must account for the possibility that it is absent:

usage.ts

declare const user: UserWithOptionalName;

const name = user.displayName;
// string | undefined when strictNullChecks is enabled
Enter fullscreen mode Exit fullscreen mode

That undefined is a JavaScript read result; JSON itself has no undefined value. A missing JSON key becomes an undefined result when you access it as an object property.

With exactOptionalPropertyTypes, TypeScript also distinguishes an omitted optional property from explicitly assigning undefined unless undefined is included in the declared value type. This option is useful when the presence of a property has meaning in your application.

What | null means

A union with null allows a present property to hold a null value:

profile.ts

interface UserProfile {
  displayName: string | null;
}

const profile: UserProfile = { displayName: null };
Enter fullscreen mode Exit fullscreen mode

The property is still required. This object does not satisfy UserProfile because the key is missing:

profile.ts

const profile: UserProfile = {};
// Type error: displayName is required
Enter fullscreen mode Exit fullscreen mode

Use T | null when the API contract explicitly uses null to represent a known state, such as no assigned owner or no value available. Do not assume that a null in one example means the property can also be absent.

When both states are allowed

Some APIs allow the field to be omitted and also return null when it is present but has no value. If the API documentation or observed contract supports both cases, model them explicitly:

profile.ts

interface UserProfile {
  displayName?: string | null;
}
Enter fullscreen mode Exit fullscreen mode

Consumers must then handle all three outcomes: a string, null, or undefined after reading an omitted property.

labels.ts

function labelFor(user: UserProfile): string {
  if (user.displayName == null) {
    return "Unnamed";
  }

  return user.displayName;
}
Enter fullscreen mode Exit fullscreen mode

The == null check intentionally matches both null and undefined. If your logic needs to distinguish an absent key from an explicitly null key, test property presence separately with Object.hasOwn(user, "displayName") and then handle the value.

How to infer the right type from JSON

A converter can infer types from the values in the sample. It cannot infer every possible response state from a single object. A property that appears with a string in your sample may be absent in another response; a property that is null may be nullable, or the sample may simply not contain a non-null example.

  • Check the API's official schema or response documentation for required properties and nullability.
  • Compare multiple realistic responses, including empty, partial, and error responses where relevant.
  • Keep optionality and nullability separate in the type declaration.
  • Add a runtime check when the incoming payload is not guaranteed to match the documented contract.

The Letinix converter documents optional-key inference when objects in the same array have different keys, and it maps a field that is null in some array objects and has a value in others to a union with null. Those are useful inferences from the supplied sample; they do not replace the API specification.

Common modeling mistakes

  • Using ? to mean nullable. field?: string does not permit a present null value.
  • Using | null to mean optional. field: string | null still requires the property to exist.
  • Making every property optional. This can hide contract mismatches and make consumers handle states the API never permits.
  • Trusting one sample as a complete schema. Use authoritative documentation or more representative responses.
  • Assuming the type validates incoming JSON. Type declarations do not validate network data at runtime.

References

TypeScript Handbook: Object Types

TypeScript 4.4 release notes: exact optional property types

Letinix JSON to TypeScript Converter


This article was originally published on Letinix.

Top comments (0)