Handling Optional Values in TypeScript.
Posted:Key takeaways:
- Prefer
??
to||
- Prefer
foo?.bar
tofoo && foo.bar
In this post, we'll dive into three separate but similar syntaxes related to values that might not be defined:
- Optional Properties (?:)
- Nullish Coalescing Operator (??)
- Optional Chaining (?.)
# Optional Properties (?:)
TypeScript Docs: Optional Properties
TypeScript supports optional properties via the following syntax:
// Works with 'interface'
interface User {
name: string;
age?: number;
}
// and also with 'type'
type User = {
name: string;
age?: number;
};
This defines a User
, having a name
and age
. But whereas name
is required, age
is optional:
// Ash Ketchum is ten years old
const ash: User = { name: "Ash Ketchum", age: 10 };
// But Rupert doesn't want to tell you his age and that's OK
const rupert: User = { name: "Rupert McKay" };
When defining a function that accepts the User
type, the age
type will be number | undefined
:
// Lets find out if this user is old enough to drive
function isOldEnoughToDrive(user: User) {
// initially user.age is number | undefined
if (user.age === undefined) {
// But after an if check the type will be narrowed
// In this example we return 'false' if 'age' isn't available.
return false;
}
// But if age is available, then it is narrowed to a 'number' type.
return user.age >= DRIVING_AGE;
}
isOldEnoughToDrive({ name: "Ash Ketchum", age: 10 }); // returns false
isOldEnoughToDrive({ name: "Rupert McKay" }); // returns false
isOldEnoughToDrive({ name: "Bilbo Baggins", age: 111 }); // returns true
This behavior is nearly equivalent to if we had:
interface User {
name: string;
age: number | undefined;
}
The difference is whether the key must be explicitly provided by objects wanting to satisfy this interface:
interface User {
name: string;
age: number | undefined;
}
// Type error! "age" is a required property of "User"
const rupert: User = { name: "Rupert McKay" };
// This works... but I really don't like seeing explicit "undefined"
const rupert: User = { name: "Rupert McKay", age: undefined };
The Optional Property syntax defines properties we can omit. Whatever type we provide will default to undefined
if the key is omitted.
# Nullish Coalescing Operator (??)
MDN: Nullish coalescing operator
The Nullish Coalescing Operator
provides a default value in case our original value is undefined or null.
const postTitle = post.title ?? "Rupert's blog post";
This will look familiar to JavaScript programmers who are used to using the OR operator (||
) to provide default values. And most of the time it works the same.
The only difference is that ||
will fall back if the lefthand value is anything falsey. Whereas ??
will only fall back if the lefthand value is null
or undefined
. Often this works out the same, but there are some common gotchas, for example, what if our lefthand value is zero or the empty string? Perhaps I wanted to post a blog that has an intentionally blank title? If I were using ||
this would be impossible. Whereas with ??
, we can still provide an empty string value, while correctly using a default value in case of undefined
.
I highly recommend forgetting about ||
for default fallbacks and using ??
instead.
You can enforce this with typescript-eslint's own prefer-nullish-coalescing rule
# Optional Chaining (?.)
What if this time we have a Session
object, which has a User
only if the current visitor is logged in:
interface Session {
user?: User;
}
function getWelcomeMessage(session: Session) {
const userName = session.user?.name;
if (userName === undefined) {
return "Hello Guest!";
}
return `Hello ${userName}!`;
}
This is effectively a shorthand for:
const userName = user && user.name;
There is one small difference though, in that the &&
style means our userName
type will be possibly any falsey value that user
could return. Whereas by using ?.
we can express our intent more concisely, and when the value is not available the whole expression fallbacks to undefined
.
# All together
We can use all these concepts together:
interface Session {
user?: User;
}
function getWelcomeMessage(session: Session) {
const userName = session.user?.name ?? "GUEST";
return `Hello ${userName}!`;
}
This is the same as the previous getWelcomeMessage
but even more concise.
# Summary
- Prefer
??
to||
- Prefer
foo?.bar
tofoo && foo.bar
Take care,
Rupert