Everyday TypeScript: Types for Options Objects
Welcome to the Types for Options Objects lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Sometimes a function needs to take many different optional parameters. We could write a function with 9 optional parameters, but calling it would be very cumbersome and error prone. (Is
retriesthe 7th or the 8th parameter? Don't forget to passundefinedfor parameters 1 through 6 since you don't need them!)JavaScript and TypeScript functions often solve this by taking "options parameters". This is a technique where we pack the extra parameters up into an object, then pass the object to the function.
This lesson shows a few different ways to statically type options parameters. We don't introduce any new TypeScript features. Instead, we combine several features that we've already seen. Our example is a
sendEmailfunction that takes two configurable options.The first option is: do we want link tracking or not? "Link tracking" means that our email provider replaces all of the links in the email automatically. That lets us see what percentage of users clicked the links.
The second option is: how many times should we retry if there's a network error? Networks inevitably fail, and some third-party services are more stable than others. The simplest workaround for network failures is a retry.
Our function accepts those two options via an
optsparameter, which is an object withtrackingandretriesproperties. The most obvious type for this object is{tracking: boolean, retries: number}.(In our code examples, we'll leave out some parameters that would be required when sending real emails, like "subject" and "body". In this lesson, we're only interested in the
optsobject. And sending actual emails would be a very complicated distraction, so we'll return strings fromsendEmailrather than talking to any real network services.)>
function sendEmail(to: string, opts: {tracking: boolean, retries: number}) {return `Emailing ${to}, tracking=${opts.tracking}, retries=${opts.retries}`;}sendEmail('amir@example.com', {tracking: true, retries: 3});Result:
That version works, but it requires every
sendEmailcall to specify both options. In most cases, we'd rather omit the options object, and have the function use some reasonable default like{tracking: true, retries: 3}. Fortunately, we can use TypeScript's default parameter values to add those default options.There are many ways to write this function signature. We'll start with everything crammed into the function signature itself: we provide the
optsparameter type inline, then we provide the default object inline as well. Later in this lesson, we'll see ways to shorten this signature.>
function sendEmail(to: string,opts: {tracking: booleanretries: number} = {tracking: true,retries: 3,}) {return `Emailing ${to}, tracking=${opts.tracking}, retries=${opts.retries}`;}sendEmail('amir@example.com');Result:
'Emailing amir@example.com, tracking=true, retries=3'
That's an improvement, but there's still a problem. With these types, we can specify both of the options, or neither of them. If we try to specify only one option, that violates
opts's object type, which is a type error.>
function sendEmail(to: string,opts: {tracking: booleanretries: number} = {tracking: true,retries: 3,}) {return `Emailing ${to}, tracking=${opts.tracking}, retries=${opts.retries}`;}sendEmail('amir@example.com', {tracking: false});Result:
type error: Argument of type '{ tracking: false; }' is not assignable to parameter of type '{ tracking: boolean; retries: number; }'. Property 'retries' is missing in type '{ tracking: false; }' but required in type '{ tracking: boolean; retries: number; }'.We want the options to feel like they're independent: we want to be able to specify none, or just one, or all of them. In terms of types, that means that each property should be optional, like
tracking?: booleanandretries?: number. When we call the function, we can providetracking,retries, neither, or both. We still include a default value for the options, but now it's{}.Optional object properties are automatically unioned with
undefinedbecause the property may be absent. Our optional properties have types likeboolean | undefinedornumber | undefined.When we unpack the options object into individual variables, we can substitute default values with the
??operator that we saw in an earlier lesson. (Remember thatx ?? 5means "ifxisnullorundefined, return5; otherwise, returnx".)>
/* Callers can specify none, some, or all of `opts`. We'll use defaultvalues for any options that are missing. */function sendEmail(to: string,opts: {tracking?: booleanretries?: number} = {}) {const tracking = opts.tracking ?? true;const retries = opts.retries ?? 3;return `Emailing ${to}, tracking=${tracking}, retries=${retries}`;}sendEmail('amir@example.com');Result:
'Emailing amir@example.com, tracking=true, retries=3'
>
function sendEmail(to: string,opts: {tracking?: booleanretries?: number} = {}) {const tracking = opts.tracking ?? true;const retries = opts.retries ?? 3;return `Emailing ${to}, tracking=${tracking}, retries=${retries}`;}sendEmail('amir@example.com', {tracking: false});Result:
'Emailing amir@example.com, tracking=false, retries=3'
There are many other ways to specify default values, and choosing between them is a matter of preference. For example, we can destructure the individual option properties inside the function signature itself.
(Destructuring is a JavaScript feature that we don't cover in detail in this course. If it's not familiar, we recommend our Modern JavaScript course, which does cover it in detail.)
>
function sendEmail(to: string,{tracking = true,retries = 3,}: {tracking?: booleanretries?: number} = {}) {return `Emailing ${to}, tracking=${tracking}, retries=${retries}`;}sendEmail('amir@example.com');Result:
'Emailing amir@example.com, tracking=true, retries=3'
>
function sendEmail(to: string,{tracking = true,retries = 3,}: {tracking?: booleanretries?: number} = {}) {return `Emailing ${to}, tracking=${tracking}, retries=${retries}`;}sendEmail('amir@example.com', {retries: 0});Result:
'Emailing amir@example.com, tracking=true, retries=0'
Maybe that long, multi-line function signature bothers you. If so, we could choose to define an
EmailOptionstype, then move the option handling back into the function body. With those changes, the function signature fits on one line. The total number of lines is about the same as before, but you may feel drawn to one style or the other.>
type EmailOptions = {tracking?: booleanretries?: number};function sendEmail(to: string, opts: EmailOptions = {}) {const tracking = opts.tracking ?? true;const retries = opts.retries ?? 3;return `Emailing ${to}, tracking=${tracking}, retries=${retries}`;}sendEmail('amir@example.com', {retries: 2});Result:
'Emailing amir@example.com, tracking=true, retries=2'
Here's a code problem:
The
initializeErrorTrackerfunction below sets up an error tracking service like Sentry or Bugsnag. Itsoptsparameter currently has a simple object type where all properties are required. Modifyopts's type so that all options are optional, and the default is no options,{}. That gives us more flexibility when calling the function.function initializeErrorTracker(opts: {sessions?: booleanappType?: string} = {}) {const sessions: boolean = opts.sessions ?? true;const appType: string = opts.appType ?? 'client';return `Tracking: sessions=${sessions}, appType=${appType}`;}[initializeErrorTracker(),initializeErrorTracker({sessions: false}),initializeErrorTracker({appType: 'server'}),initializeErrorTracker({sessions: false, appType: 'server'}),];- Goal:
['Tracking: sessions=true, appType=client', 'Tracking: sessions=false, appType=client', 'Tracking: sessions=true, appType=server', 'Tracking: sessions=false, appType=server']
- Yours:
['Tracking: sessions=true, appType=client', 'Tracking: sessions=false, appType=client', 'Tracking: sessions=true, appType=server', 'Tracking: sessions=false, appType=server']
We have a lot of flexibility in how we combine optional properties, default parameter values, and object destructuring. That flexibility nicely demonstrates one of TypeScript's primary language design goals.
Some languages are designed to be simple and direct, with a small, elegant set of core features. TypeScript isn't one of those languages. TypeScript is designed to accommodate all of the weird JavaScript code that already exists out in the world. Existing JavaScript code involves many options object styles, so TypeScript lets us express types for all, or at least most, of those styles.