← Back

Don’t be optional, be explicit

Why you shouldn’t always use ? in Typescript

Picture of Wyatt Johnson
Wyatt Johnson

A popular feature in Typescript lets you mark a given property as optional in a type or interface, these are called optional properties:

1type Props = {
2 inverted?: boolean;
3 color: string;
4}

These are pretty useful in lots of circumstances as it allows you to keep the code clean and simple but still allow some flexibility when it comes to calling functions and defining objects. In React for example, we use optional boolean properties on Props in order to make them flexible:

1<Button color="red" />
2<Button color="blue" inverted />

The Problem

This works great in these cases because the number of properties is usually pretty small. In larger codebases however where these optional properties are used as arguments for functions, things can get a little more complicated:

1type Context = {
2 // ... 10 other properties
3 cache?: IncrementalCache;
4}
5
6function build(ctx: Context): void

The problem in this case is that there’s a risk that by not defining or passing each parameter, you could have different behaviours. With the advent of the optional chaining operator (?.), code that feels natural to write can have odd consequences:

1function build(ctx: Context): void {
2 // ...
3 const value = ctx.cache?.get(key)
4 // ...
5}

In this case, the code above “optionally” gets the value from the cache if the cache is provided. This makes sense from within the function’s scope, but outside, this tradeoff isn’t quite that clear.

Alternative

Rather than relying on optional properties, you could instead still leverage the optional chaining operator by explicitly requiring that the value be specified, even if it’s not available:

1type Context = {
2 // ...
3 cache: IncrementalCache | undefined;
4}

This forces developers to specify the absence of the parameter rather than letting it silently get ignored, leading to the following:

1// With cache?: IncrementalCache
2build({ /* ... */ })
3build({ cache, /* ... */ })
4
5// With cache: IncrementalCache | undefined
6build({ /* ... */ }) // Property 'cache' is missing
7build({ cache: undefined, /* ... */ })
8build({ cache, /* ... */ })

Being explicit with the properties passed allows users to always see explicitly what’s being passed to the function rather then being silent. This is even compatible with the optional chaining operator as it forces users to still pass the property but it will return undefined if the property is not defined:

1let ctx: { cache?: IncrementalCache } | undefined
2
3build({ cache: ctx?.cache })

I’m definitely not advocating for all properties to be explicit, but for those involved with critical control flows in functions and your applications, you’re probably better off being explicit with those properties instead of leaving them optional.