Caveats of processing TypeScript code with Babel

vire

Viliam

Posted on February 24, 2020

Caveats of processing TypeScript code with Babel

tl;dr When writing classes/OOP in TypeScript --strict should definitely be enabled, or at minimum use --strictPropertyInitialization as it might save hours of debugging in scenarios where tsc is replaced with babel. Second thing I wish I knew is that the order of plugins in babel.config.js affects the output.

My initial goal was to decrease unit tests execution of cca. 4500 tests taking 340 seconds. First what came to my mind was to modify jest config especially how the code is being transformed.

The idea was to utilize tsc only for typechecking and babel for transpilation. Why? Because babel was already in place by applying the emotion-plugin and therefore doing the transpilation only via babel could shave some time off.

Obviously during the process of config upgrade & tuning I've hit a few snags which I'd like to share - as someone after reading might not run into similar problems.

Changes in jest config

NOTE: there was a shared babel.config.js for both unit and functional-browser tests setup plus they each had s separate jest config

BEFORE

  1. typechecked and compiled .tsx? using tsc + .jsx? transpiled using babel see ts-jest/presets/js-with-babel

AFTER

  1. typechecked in a separate npm script using tsc --noEmit
  2. transpiled by babel-jest / babel.config.js with plugins processing typescript code
  const presets = [
    ['@babel/preset-typescript', { allowNamespaces: true }],
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'entry',
        corejs: 3,
        modules: 'commonjs',
        loose: true,
      },
    ],
  ]
  const plugins = [
    'babel-plugin-const-enum',
    '@babel/plugin-proposal-optional-chaining',
    '@babel/plugin-proposal-nullish-coalescing-operator',
    ["@babel/plugin-proposal-class-properties", { loose: true }],
    ["@babel/plugin-transform-typescript", { allowNamespaces: true }],
  ];

Oops, there is an error

Everything seemed to be working fine at least for unit tests but life's though and the first error appeared during functional-browser tests run saying:

TypeError: Cannot read property 'getElement' of undefined

That particular stack trace was pointing to one of the Page Object base classes in which a super-class instance property was accessed from ancestor.

Process of investigation

My first hypothesis was that this error might be related to the compilation target setting, specifically with regard to how babel transpiles classes. By trying out a couple of different possible values for target, it quickly became apparent that that was not the case.

Next suspect in down the call stack were few async/await functions which turned in a "small sync test reproduction" this hypothesis out of play as well.

Finally after putting explicit console.log into constructors all the way up the prototype chain as well as passing different init arguments, it pointed me to the right direction. Also a good move was to remove TypeScript's class member declarations - that just confirmed the suspicion -that these properties with type annotation were incorrectly transpiled (the "how" will be explained in following section).

The final puzzle-piece was found during verification of transpiled output as it was clearly visible that the child's class constructor did override/initialized the parent's property with void 0

Root cause breakdown

A trivial example replicating the scenario - a parent class + a child class that relies on parent's property but it's undefined when transpiling with babel
ts-source

The output transpiled by babel

babel-out

For comparison the same code compiled with TypeScript works just fine

working-ts-example

Finally the culprit busted - incorrect plugin order

babel-config-incorrect

Confirmation that swapping the order of plugins produces different output

babel-config-correct

Finally the problem could be easily prevent with strict mode or strictPropertyInitialization enabled.

Alt Text

Complexity reactor

is a sequence of related and non-related events contributing individually or combined to raise overall complexity.

  • permutations
    • unit vs functional-browser tests
    • babel config - presets, plugins, settings, env, targets
  • causation fallacy - thx indiegate

    With tsc everything compiles fine, with babel a class instance property in prototype chain is undefined AND therefore the different compiler implementation typescript -> babel is the cause.

    But the real cause was a wrong order of plugins in babel config FACT: one of the plugins isn't TypeScript related at all. FACT: with a strict TypeScript config / Strict Property Initialization this error would not occur in first place.

  • dead-end exploration

    • suspect: constructor augmentation using 3rd party mixin
  • tedious debugging (try/catch approach), long turn-arounds

  • OOP / multi-level inheritance used in Page Object implementation

  • ES6 classes/this/super()

  • non-strict tsconfig.json mode / important flags not enabled

  • problems where ordering matters see Typescript plugin overwrites parent class properties when the child narrows that property's type

Conclusions

💖 💪 🙅 🚩
vire
Viliam

Posted on February 24, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related