Skip to content
This repository has been archived by the owner on Feb 26, 2021. It is now read-only.
/ ts-optchain Public archive

Optional Chaining for TypeScript

License

Notifications You must be signed in to change notification settings

rimeto/ts-optchain

Repository files navigation


This project is now deprecated with Optional Chaining support included in TypeScript 3.7 and later.

Optional Chaining for TypeScript

The ts-optchain module is an implementation of optional chaining with default value support for TypeScript. ts-optchain helps the developer produce less verbose code while preserving TypeScript typings when traversing deep property structures. This library serves as an interim solution pending JavaScript/TypeScript built-in support for optional chaining in future releases (see: Related Resources).

This module includes two optional chaining implementations:

  • ES6 Proxy Implementation: trivial setup, but incompatible with legacy browsers, such as IE 11.
  • TypeScript Custom Code Transformer: faster performance and compatible with legacy browsers.

Installation

npm i --save ts-optchain

ES6 Proxy

No additional configuration is required to use the ES6 Proxy implementation of ts-optchain.

The ES6 Proxy implementation of ts-optchain requires NodeJS >= 6 or compatible JS environment

IMPORTANT: ES6 Proxy is NOT supported by many legacy browsers, including IE 11 and older versions of ReactNative Android!

Consider using one of the following alternative implementations if support for legacy browsers is a requirement.

TypeScript Custom Code Transformer

TTypescript is a tool allows the developer to apply the TypeScript custom transformer automatically at build time. Configuration is as simple as adding the plugins property to compilerOptions in tsconfig.json, e.g.:

// tsconfig.json
{
    "compilerOptions": {
        "plugins": [
            { "transform": "ts-optchain/transform" },
        ]
    },
}

The developer can then build + transform via the command line, webpack, ts-node, etc. Please see the usage instructions.

After setup, the code:

  import { oc } from 'ts-optchain';
  const obj: T = { /* ... */ };
  const value = oc(obj).propA.propB.propC(defaultValue);

...will be automatically transformed to:

  const value =
    (obj != null && obj.propA != null && obj.propA.propB != null && obj.propA.propB.propC != null)
      ? obj.propA.propB.propC
      : defaultValue;

Babel Plugin

For developers using babel with a need for legacy browser support, consider using the derivative project babel-plugin-ts-optchain.

Example Usage

import { oc } from 'ts-optchain';

interface I {
  a?: string;
  b?: {
    d?: string;
  };
  c?: Array<{
    u?: {
      v?: number;
    };
  }>;
  e?: {
    f?: string;
    g?: () => string;
  };
}

const x: I = {
  a: 'hello',
  b: {
    d: 'world',
  },
  c: [{ u: { v: -100 } }, { u: { v: 200 } }, {}, { u: { v: -300 } }],
};

// Here are a few examples of deep object traversal using (a) optional chaining vs
// (b) logic expressions. Each of the following pairs are equivalent in
// result. Note how the benefits of optional chaining accrue with
// the depth and complexity of the traversal.

oc(x).a(); // 'hello'
x.a;

oc(x).b.d(); // 'world'
x.b && x.b.d;

oc(x).c[0].u.v(); // -100
x.c && x.c[0] && x.c[0].u && x.c[0].u.v;

oc(x).c[100].u.v(); // undefined
x.c && x.c[100] && x.c[100].u && x.c[100].u.v;

oc(x).c[100].u.v(1234); // 1234
(x.c && x.c[100] && x.c[100].u && x.c[100].u.v) || 1234;

oc(x).e.f(); // undefined
x.e && x.e.f;

oc(x).e.f('optional default value'); // 'optional default value'
(x.e && x.e.f) || 'optional default value';

// NOTE: working with function value types can be risky. Additional run-time
// checks to verify that object types are functions before invocation are advised!
oc(x).e.g(() => 'Yo Yo')(); // 'Yo Yo'
((x.e && x.e.g) || (() => 'Yo Yo'))();

Problem

When traversing tree-like property structures, the developer often must check for existence of intermediate nodes to avoid run-time exceptions. While TypeScript is helpful in requiring the necessary existence checks at compile-time, the final code is still quite cumbersome. For example, given the interfaces:

interface IAddress {
  street?: string;
  city?: string;
  state?: string;
  postalCode?: string;
}

interface IHome {
  address?: IAddress;
  phoneNumber?: string;
}

interface IUser {
  home?: IHome;
}

Without support for optional chaining built into TypeScript yet, an implementation for a method to extract the home street string from this structure would look like:

function getHomeStreet(user: IUser, defaultValue?: string) {
  return (user.home && user.home.address && user.home.address.street) || defaultValue;
}

This implementation is tedious to write. Utilities like lodash's get(...) can help tighten the implementation, namely:

import { get } from 'lodash';

function getHomeStreet(user: IUser, defaultValue?: string) {
  return get(user, 'home.address.street', defaultValue);
}

However, when using tools like lodash the developer loses the benefits of:

  • Compile-time validation of the path home.address.street
  • Compile-time validation of the expected type of the value at home.address.street
  • Development-time code-completion assistance when manipulating the path home.address.street using tools like Visual Studio Code.

Solution

Using the ts-optchain utility, getHomeStreet can be concisely written as:

import { oc } from 'ts-optchain';

function getHomeStreet(user: IUser, defaultValue?: string) {
  return oc(user).home.address.street(defaultValue);
}

Other features of ts-optchain include:

Type Preservation

ts-optchain preserves TypeScript typings through deep tree traversal. For example:

// phoneNumberOptional is of type: string | undefined
const phoneNumberOptional = oc(user).home.phoneNumber();

// phoneNumberRequired is of type: string
const phoneNumberRequired = oc(user).home.phoneNumber('+1.555.123.4567');

Array Types

ts-optchain supports traversal of Array types by index. For example:

interface IItem {
  name?: string;
}

interface ICollection {
  items?: IItem[];
}

function getFirstItemName(collection: ICollection) {
  // Return type: string
  return oc(collection).items[0].name('No Name Item');
}

Function Types

ts-optchain supports traversal to function values. For example:

interface IThing {
  getter?: () => string;
}

const thing: IThing = { ... };
const result = oc(thing).getter(() => 'Default Getter')();

Code-Completion

ts-optchain enables code-completion assistance in popular IDEs such as Visual Studio Code when writing tree-traversal code.

Benchmarks

Comparing the ES6 Proxy implementation vs the TypeScript custom transformer implementation.

Test case:

oc(testData).a.b.c();

Results:

ts-optchain ts-optchain/transform
Chrome 72 2,352,109 ops/s ±1.16% 628,693,809 ops/s ±0.44% 267x
Safari 12 752,298 ops/s ±1.47% 1,760,808,177 ops/s ±0.93% 2,340x
Firefox 65 272,155 ops/s ±4.78% 793,869,896 ops/s ±0.82% 2,916x

Related Resources

License

ts-optchain is MIT Licensed.