Marius Schulz
Marius Schulz
Front End Engineer

Typing Destructured Object Parameters in TypeScript

In TypeScript, you can add a type annotation to each formal parameter of a function using a colon and the desired type, like this:

function greet(name: string) {
  return `Hello ${name}!`;
}

That way, your code doesn't compile when you attempt to call the function with an argument of an incompatible type, such as number or boolean. Easy enough.

Let's now look at a function declaration that makes use of destructuring assignment with an object parameter, a feature that was introduced as part of ECMAScript 2015. The toJSON function accepts a value of any type that should be stringified as JSON. It additionally accepts a settings parameter that allows the caller to provide configuration options via properties:

function toJSON(value: any, { pretty }) {
  const indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

The type of the value parameter is explicitly given as any, but what type does the pretty property have? We haven't explicitly specified a type, so it's implicitly typed as any. Of course, we want it to be a boolean, so let's add a type annotation:

function toJSON(value: any, { pretty: boolean }) {
  const indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

However, that doesn't work. The TypeScript compiler complains that it can't find the name pretty that is used within the function body. This is because boolean is not a type annotation in this case, but the name of the local variable that the value of the pretty property gets assigned to. Again, this is part of the specification of how object destructuring works.

Because TypeScript is a superset of JavaScript, every valid JavaScript file is a valid TypeScript file (set aside type errors, that is). Therefore, TypeScript can't simply change the meaning of the destructuring expression { pretty: boolean }. It looks like a type annotation, but it's not.

#Typing Immediately Destructured Parameters

Of course, TypeScript offers a way to provide an explicit type annotation. It's a little verbose, yet (if you think about it) consistent:

function toJSON(value: any, { pretty }: { pretty: boolean }) {
  const indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

You're not directly typing the pretty property, but the settings object it belongs to, which is the actual parameter passed to the toJSON function. If you now try to compile the above TypeScript code, the compiler doesn't complain anymore and emits the following JavaScript function:

function toJSON(value, _a) {
  var pretty = _a.pretty;
  var indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

#Providing Default Values

To call the above toJSON function, both the value and the settings parameter have to be passed. However, it might be reasonable to use default settings if they aren't explicitly specified. Assuming that pretty should be true by default, we'd like to be able to call the function in the following various ways:

const value = { foo: "bar" };

toJSON(value, { pretty: true }); // #1
toJSON(value, {}); // #2
toJSON(value); // #3

The function call #1 already works because all parameters are specified. In order to enable function call #2, we have to mark the pretty property as optional by appending a question mark to the property name within the type annotation. Additionally, the pretty property gets a default value of true if it's not specified by the caller:

function toJSON(value: any, { pretty = true }: { pretty?: boolean }) {
  const indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

Finally, function call #3 is made possible by providing a default value of {} for the destructuring pattern of the settings object. If no settings object is passed at all, the empty object literal {} is being destructured. Because it doesn't specify a value for the pretty property, its fallback value true is returned:

function toJSON(value: any, { pretty = true }: { pretty?: boolean } = {}) {
  const indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

Here's what the TypeScript compiler emits when targeting "ES5":

function toJSON(value, _a) {
  var _b = (_a === void 0 ? {} : _a).pretty,
    pretty = _b === void 0 ? true : _b;
  var indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

When targeting "ES6", only the type information is removed:

function toJSON(value, { pretty = true } = {}) {
  const indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}

#Extracting a Type for the Settings Parameter

With multiple properties, the inline type annotation gets unwieldy quickly, which is why it might a good idea to create an interface for the configuration object:

interface SerializerSettings {
  pretty?: boolean;
}

You can now type the settings parameter using the new interface type:

function toJSON(value: any, { pretty = true }: SerializerSettings = {}) {
  const indent = pretty ? 4 : 0;
  return JSON.stringify(value, null, indent);
}