TypeScript exactOptionalPropertyTypes in Practice
exactOptionalPropertyTypes is a compiler flag that treats an absent optional member and a member holding undefined as two different states. Under the flag, an optional member accepts a value of its stated type or no member at all.
exactOptionalPropertyTypes keeps an absent member absent
The profile is built without the nickname member. The in check reports the member as absent, which the flag treats as a state of its own.
interface Profile {
userName: string;
nickname?: string;
}
const profile: Profile = { userName: 'Asha' };
console.log('nickname' in profile);
console.log(profile.userName);exactOptionalPropertyTypes refuses undefined for an optional member
The literal sets nickname to undefined while the member names only string. Under the flag the checker refuses the literal.
interface Profile {
userName: string;
nickname?: string;
}
const profile: Profile = { userName: 'Asha', nickname: undefined };
console.log(profile.userName);exactOptionalPropertyTypes admits undefined with the | undefined spelling
The member is spelled with an added undefined in its type. The literal now compiles under the flag, and in reports the member as present.
interface Profile {
userName: string;
nickname?: string | undefined;
}
const profile: Profile = { userName: 'Asha', nickname: undefined };
console.log('nickname' in profile);
console.log(profile.nickname === undefined);exactOptionalPropertyTypes accepts a member added only when present
The builder starts with an absent member and adds the member only when a value arrives. Both profiles compile under the flag, and the in checks show the two states.
interface Profile {
userName: string;
nickname?: string;
}
function buildProfile(userName: string, nickname?: string): Profile {
const profile: Profile = { userName };
if (nickname !== undefined) {
profile.nickname = nickname;
}
return profile;
}
const plain = buildProfile('Asha');
console.log('nickname' in plain);
const named = buildProfile('Ben', 'Benny');
console.log('nickname' in named);
if ('nickname' in named) {
console.log(named.nickname);
}Example
interface Settings {
theme: string;
banner?: string;
note?: string | undefined;
}
const plain: Settings = { theme: 'dark' };
console.log('banner' in plain);
const withNote: Settings = { theme: 'dark', note: undefined };
console.log('note' in withNote);
console.log(withNote.note === undefined);
console.log(plain.theme);Key points
- Without the flag, an optional member written as nickname?: string also accepts an explicit undefined.
- Under the flag, nickname?: string accepts a string or an absent member, and nothing else.
- The in operator tells the two states apart, because a member holding undefined is still present.
- Spelling the member as nickname?: string | undefined admits both states under the flag.
- Conditional construction builds an object with the member only when a value exists, which keeps absence honest.
In this example, one settings object leaves a member absent while another holds an explicit undefined under the wider spelling, and the in checks print the difference between the two states.
Note
The flag is separate from strict mode, and a project turns it on with its own line in tsconfig.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท TypeScript Playground
Exercise
Under exactOptionalPropertyTypes, which spelling lets an optional member hold an explicit undefined?