Headshot of Rupert

Rupert McKay

Handling Optional Values in TypeScript.

Posted: 10 Jul 2021

Key takeaways:

In this post, we'll dive into three separate but similar syntaxes related to values that might not be defined:

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 (?.)

MDN: 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

Take care,

Rupert