../

TypeScript performance tip—Escaping partial

└─ 2018-09-25 • Reading time: ~5 minutes

A common pattern in JavaScript or TypeScript is to pass options to a function or constructor as an object with optional attributes (meaning, only a subset of the attributes can be specified). Since v2.1, TypeScript offers a great way to make this type-safe in the form of Partial<T> which allows to specify a type for exactly this pattern. In practice this looks like:

interface IOptions {
  option1: boolean;
  option2: string;
}

function doSomething(options: Partial<IOptions> = {}): void {
  ...
}

This definition allows to give a subset of the options to the function, or all of them, or no argument at all!

doSomething(); // OK
doSomething({ option1: true }); // OK
doSomething({ option1: true, option2: 'foo' }); // OK

doSomething({ option3: 42 }); // NOT OK

Now, something you might want to do is to provide a set of default values for each attribute of your IOptions passed as argument. One way this could be done is to use Object.assign to merge the partial options given as argument with a set of default values:

function setDefaults(options: Partial<IOptions> = {}): IOptions {
  return Object.assign({
    option1: true,
    option2: 'bar',
  }, options);
}

This is elegant and it works. However, this is not the optimal solution in terms of performance. There is still some overhead in the call to assign, and although it might not matter most of the time, sometimes you just want to make things go as fast as possible. As a baseline, let’s measure the performance of this solution using the benchmark library:

14M calls/second

To by-pass the use of Object.assign, we could try to use the spread operator:

function setDefaults(options: Partial<IOptions> = {}): IOptions {
  return {
    option1: true,
    option2: 'bar',
    ...options,
  };
}

This is even terser, but unfortunately it’s not as fast, at least when esnext is targeted. For ES6 or lower, a call to Object.assign will be made, which makes this solution equivalent to the first one.

7M calls/second (x0.6)

Another solution would be to hard-code the creation of the object with the existence of each attribute checked:

function setDefaults(options?: Partial<IOptions>): IOptions {
  return {
    option1: options.option1 !=== undefined ? options.options1 : true,
    option2: options.option2 !=== undefined ? options.options2 : 'bar',
  };
}

This does the job, but is much more verbose, and you will have to write the name of each attribute several times. It does not get better as your option type grows. The performance is much better though:

215M calls/second (x15)

If you already looked at the code generated by Babel or TypeScript to transpile arguments destructuring, this should look familiar. We could try to make TypeScript generate the boilerplate code for us by targeting an older version of ECMAScript (which does not support destructuring). On top of that, destructuring allows to specify default values for some (or all) of the attributes. Let’s combine these two ideas to create a fourth version of our setDefaults function:

function setDefaults({
  option1 = true,
  option2 = 'bar',
}: Partial<IOptions> = {}): IOptions {
  return { option1, option2 };
}

This is almost as terse as the first version based on Object.assign and much better than the second version. It is also the fastest version:

230M calls/second (x16)

Out of curiosity, we can check the code generated by TypeScript depending on the target you specify. Keep in mind that the performance seen above corresponds to the ES3 target.

  • ES3 and ES5
function setDefaults(_a) {
  var _b = _a === void 0 ? {} : _a, _c = _b.option1, option1 = _c === void 0 ? true : _c, _d = _b.option2, option2 = _d === void 0 ? 'bar' : _d;
  return { option1: option1, option2: option2 };
}

Yey, fast and ugly!

230M calls/second (x16)

  • ES6
function setDefaults({ option1 = true, option2 = 'bar', } = {}) {
  return { option1, option2 };
}

Yey, nice and not as fast…

170M calls/second (x12)

Here we can see that the implementation of ES6 destructuring with defaults is not yet as fast as the transpiled version, at least on V8 (Node.js 10.11.0). In practice that is not an issue as TypeScript will allow you to write high-level, type-safe code, while still giving you the best performance by targeting low-level JavaScript (e.g.: ES3!).

The full source-code for benchmarks can be found there: typescript-options-bench.ts. All the results were obtained using Node.js v10.11.0 on an Intel i7-6600U (2,60-3,40 GHz) with 16GB of Ram, running Ubuntu 18.04.