Caveats of processing TypeScript code with Babel
Viliam
Posted on February 24, 2020
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
-
typechecked and compiled
.tsx?
usingtsc
+.jsx?
transpiled usingbabel
see ts-jest/presets/js-with-babel
AFTER
-
typechecked in a separate npm script using
tsc --noEmit
-
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
The output transpiled by babel
For comparison the same code compiled with TypeScript works just fine
Finally the culprit busted - incorrect plugin order
Confirmation that swapping the order of plugins produces different output
Finally the problem could be easily prevent with strict
mode or strictPropertyInitialization
enabled.
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, withbabel
a class instance property in prototype chain isundefined
AND therefore the different compiler implementationtypescript
->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 enabledproblems where ordering matters see Typescript plugin overwrites parent class properties when the child narrows that property's type
Conclusions
over raw OOP in JS/TS prefer a solution with more explicit semantic e.g. github.com/stampit-org/stampit -> Classes vs. Stamps
when debugging transpiler issues it's good to start with the simplest possible test scenario involving as few dependencies as possible. Otherwise tracing errors in the "real" setup is way more time consuming and contains a lot "moving parts" that can lead to dead end.
verify quickly the transpilation output using online tools like www.typescriptlang.org/play or babeljs.io/repl - see not working scenario and working scenario
Posted on February 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.