Skip to content
TypeScript ts objects 4 min read

Optional Properties

Not every property of an object is always present. A search filter might omit fields the user left blank; a config object might fill in defaults. TypeScript models this with the optional modifier ?, which says a property may be missing from the object. The subtlety — and a common source of bugs — is the difference between a property that can be absent and one that is required but allowed to hold undefined. This page covers the optional modifier, how it interacts with undefined, and the exactOptionalPropertyTypes flag that makes the distinction strict.

The optional modifier

Add ? after a property name to make it optional. Callers may then omit the property entirely, and TypeScript will not complain.

interface User {
  id: number;
  name: string;
  nickname?: string;
}

const a: User = { id: 1, name: "Ada" };              // ✅ nickname omitted
const b: User = { id: 2, name: "Bo", nickname: "B" }; // ✅ provided

An optional property is implicitly typed as its declared type plus undefined. So nickname has the type string | undefined, and TypeScript forces you to account for the missing case before using it as a string.

Reading optional properties

Because an optional property might be undefined, you must narrow it before using it where the non-optional type is required. Optional chaining, nullish coalescing, and plain guards all work.

function display(user: User): string {
  // ❌ Error: 'user.nickname' is possibly 'undefined'.
  // return user.nickname.toUpperCase();

  return user.nickname?.toUpperCase() ?? user.name;
}

Here ?. short-circuits to undefined when nickname is absent, and ?? supplies name as the fallback. After an explicit if (user.nickname) check, TypeScript narrows the type to string inside the block. See Truthiness Narrowing for the narrowing rules.

Optional vs | undefined

A frequent confusion: prop?: T and prop: T | undefined look similar but differ in whether the key must exist. The optional form lets you omit the key; the union form requires the key to be present, even if its value is undefined.

interface A { value?: number }       // key may be absent
interface B { value: number | undefined } // key must exist, may be undefined

const a: A = {};                 // ✅ key omitted
const b: B = {};                 // ❌ Error: 'value' is missing
const b2: B = { value: undefined }; // ✅ present and explicitly undefined
DeclarationKey may be omittedValue may be undefined
value?: numberYesYes
value: number | undefinedNoYes
value?: number | undefinedYesYes
value: numberNoNo

Use ? when the property is genuinely optional, and T | undefined when the field is mandatory but may carry no value (for example a database column that exists but can be null-like).

exactOptionalPropertyTypes

By default, TypeScript treats value?: number as number | undefined, so assigning undefined explicitly is allowed. The strict flag exactOptionalPropertyTypes separates “missing” from “set to undefined,” which better matches how in checks and Object.keys behave at runtime.

interface Settings {
  theme?: "light" | "dark";
}

// With exactOptionalPropertyTypes enabled:
const ok: Settings = {};                    // ✅ omitted
// ❌ Error: Type 'undefined' is not assignable to type '"light" | "dark"'.
const bad: Settings = { theme: undefined };

With the flag on, you can still allow an explicit undefined by declaring theme?: "light" | "dark" | undefined. Enable it for new strict codebases where the presence of a key carries meaning.

Turn on exactOptionalPropertyTypes only after strict is already enabled. It surfaces real bugs in code that conflates “key absent” with “key set to undefined,” but it can be noisy when retrofitting an older codebase.

Optional properties vs defaults

Optional properties pair naturally with default values. A default applied while destructuring removes the undefined from the local binding, so you can use it as the bare type immediately.

interface Options {
  retries?: number;
  verbose?: boolean;
}

function run({ retries = 3, verbose = false }: Options = {}) {
  // `retries` is `number`, `verbose` is `boolean` here — no undefined
  if (verbose) console.log(`retrying up to ${retries} times`);
}

run();                 // uses all defaults
run({ retries: 5 });   // overrides one

The trailing = {} makes the whole argument optional too, so run() works with no arguments. This pattern keeps the public type forgiving while the implementation sees fully resolved values.

Best Practices

  • Use ? for properties that may legitimately be absent, not as a shortcut to silence errors.
  • Remember prop?: T widens to T | undefined — always narrow before using the value.
  • Distinguish optional (key may be missing) from T | undefined (key required, value optional).
  • Supply defaults in destructuring to strip undefined from local bindings.
  • Enable exactOptionalPropertyTypes in strict projects to keep “missing” and “undefined” apart.
  • Prefer optional chaining (?.) and nullish coalescing (??) over manual undefined checks.
  • Avoid making everything optional — required fields document a genuine contract.
Last updated June 29, 2026
Was this helpful?