exactOptionalPropertyTypes

Stops an optional property from accepting an explicit undefined unless its type says so.

Signature
"exactOptionalPropertyTypes": boolean  // Default: false
  • Added in TypeScript 4.4

Parameters

ParameterTypeDescription
$exactOptionalPropertyTypesoptionalboolean= falseWhen true, a property declared with ? may be absent or hold one of its declared types, but may not be explicitly assigned undefined unless undefined is part of the declared type.

Errors

Nothing at runtime; it reports error 2412 on an undefined assignment and 2375 on an object literal with an undefined-valued optional property.

Changelog

  • PHP 4.4Introduced. It is not part of the strict family and stays off even when strict is true.

By default { theme?: "dark" | "light" } permits three values: "dark", "light" and undefined. That conflates two different states. "theme" in settings, Object.keys, JSON.stringify and object spread all distinguish a missing key from a present one holding undefined — the type system, by default, does not.

exactOptionalPropertyTypes makes ? mean exactly what it says: the property may be absent, or it may hold a declared value. To allow an explicit undefined, write it into the type: theme?: "dark" | "light" | undefined.

When to reach for it

Whenever absence is meaningful — a settings object where missing means "inherit", a patch payload where a present undefined would clear a field, or code that branches on in.

What trips people up

It is not part of strict, so it stays off unless you name it. Conditionally building an object is the usual casualty: { theme: maybeTheme } fails, and you need ...(maybeTheme ? { theme: maybeTheme } : {}) instead. Third-party .d.ts files written without the flag in mind often need | undefined added, which you cannot do from your side.

Example

Example · typescript
// "exactOptionalPropertyTypes": true
interface UserDefaults {
  colorThemeOverride?: "dark" | "light"; // absence means "system"
}

const a: UserDefaults = {};
// const b: UserDefaults = { colorThemeOverride: undefined };
// -> error 2375 with exactOptionalPropertyTypes on
const b = { colorThemeOverride: undefined } as UserDefaults;

console.log("colorThemeOverride" in a, "colorThemeOverride" in b); // false true
console.log(Object.keys(a).length, Object.keys(b).length);         // 0 1
console.log(JSON.stringify(a), JSON.stringify(b));                 // {} {}

When to use it

  • Modelling a PATCH payload where an explicitly present undefined means "clear this field"
  • Keeping a settings object where a missing key means "inherit from the parent"
  • Making code that branches on the in operator agree with the declared types

Frequently asked questions

How do I fix "Type 'undefined' is not assignable" on an optional property?

Either omit the property entirely instead of setting it to undefined, or add | undefined to its declared type — for example nickname?: string | undefined. Errors 2412 and 2375 come from exactOptionalPropertyTypes.

Does strict enable exactOptionalPropertyTypes?

No. It is a separate opt-in that has never been part of the strict family, so you must list it in compilerOptions yourself even in a fully strict project.

What is the difference between a missing property and one set to undefined?

At runtime they differ for the in operator, Object.keys, hasOwnProperty and object spread — a present undefined key is still a key. exactOptionalPropertyTypes makes the type system reflect that distinction instead of treating the two as interchangeable.

More examples

Building an object conditionally

The spread pattern is the idiomatic fix: with a value the key appears, without one it is absent rather than present-and-undefined, which is what the third log confirms.

Example · typescript
interface Query {
  page?: number;
  search?: string;
}

function build(search: string | undefined): Query {
  // { search } is error 2375 once the flag is on.
  return { page: 1, ...(search !== undefined ? { search } : {}) };
}

console.log(build("cats"));      // { page: 1, search: 'cats' }
console.log(build(undefined));   // { page: 1 }
console.log("search" in build(undefined)); // false

Opting a property back in

Adding | undefined to nickname restores the old permissiveness for that one property while age keeps the exact meaning. The flag does not remove expressiveness — it makes you state which behaviour each field has.

Example · typescript
interface Patch {
  // "absent = leave alone, undefined = clear the field"
  nickname?: string | undefined;
  age?: number; // absent only
}

const clearNickname: Patch = { nickname: undefined }; // allowed
const bumpAge: Patch = { age: 31 };
// const clearAge: Patch = { age: undefined }; // error 2375

console.log("nickname" in clearNickname, clearNickname.nickname); // true undefined
console.log(Object.keys(bumpAge)); // [ 'age' ]

Discussion

  • Be the first to comment on this lesson.