Execute Program

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 retries the 7th or the 8th parameter? Don't forget to pass undefined for 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 sendEmail function 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 opts parameter, which is an object with tracking and retries properties. 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 opts object. And sending actual emails would be a very complicated distraction, so we'll return strings from sendEmail rather 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 sendEmail call 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 opts parameter 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: boolean
    retries: 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'Pass Icon
  • 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: boolean
    retries: 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; }'.Pass Icon
  • 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?: boolean and retries?: number. When we call the function, we can provide tracking, retries, neither, or both. We still include a default value for the options, but now it's {}.

  • Optional object properties are automatically unioned with undefined because the property may be absent. Our optional properties have types like boolean | undefined or number | 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 that x ?? 5 means "if x is null or undefined, return 5; otherwise, return x".)

  • >
    /* Callers can specify none, some, or all of `opts`. We'll use default
    values for any options that are missing. */
    function sendEmail(
    to: string,
    opts: {
    tracking?: boolean
    retries?: 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'Pass Icon
  • >
    function sendEmail(
    to: string,
    opts: {
    tracking?: boolean
    retries?: 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'Pass Icon
  • 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?: boolean
    retries?: number
    } = {}
    ) {
    return `Emailing ${to}, tracking=${tracking}, retries=${retries}`;
    }

    sendEmail('amir@example.com');
    Result:
    'Emailing amir@example.com, tracking=true, retries=3'Pass Icon
  • >
    function sendEmail(
    to: string,
    {
    tracking = true,
    retries = 3,
    }: {
    tracking?: boolean
    retries?: number
    } = {}
    ) {
    return `Emailing ${to}, tracking=${tracking}, retries=${retries}`;
    }

    sendEmail('amir@example.com', {retries: 0});
    Result:
    'Emailing amir@example.com, tracking=true, retries=0'Pass Icon
  • Maybe that long, multi-line function signature bothers you. If so, we could choose to define an EmailOptions type, 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?: boolean
    retries?: 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'Pass Icon
  • Here's a code problem:

    The initializeErrorTracker function below sets up an error tracking service like Sentry or Bugsnag. Its opts parameter currently has a simple object type where all properties are required. Modify opts'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?: boolean
    appType?: 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']Pass Icon
  • 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.