exactOptionalPropertyTypes
Stops an optional property from accepting an explicit undefined unless its type says so.
"exactOptionalPropertyTypes": boolean // Default: false- Added in TypeScript 4.4
Parameters
| Parameter | Type | Description |
|---|---|---|
$exactOptionalPropertyTypesoptional | boolean= false | When 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
// "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.
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)); // falseOpting 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.
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