EsNext features in TypeScript with Babel
TypeScript is a statically typed language built on top of JavaScript. It works basically like a type layer for JS - it does not introduce new features to the JS language besides types itself and implementing few syntax proposals. But what if we could extend it further? Would you like to add some custom syntax to the TypeScript? I do. Babel already allows us to spice up JavaScript with syntax plugins. Let’s try to do the same with TypeScript
Let’s do some tests
After reading this post or cloning this repo and setting our Babel/TypeScript environment I’ll install @babel/plugin-proposal-pipeline-operator
plugin:
npm i --save-dev @babel/plugin-proposal-pipeline-operator
and add it to .babelrc
:
{ "presets": ["@babel/env", "@babel/preset-typescript"], "plugins": [ "@babel/proposal-class-properties", "@babel/proposal-object-rest-spread", // add this plugin: ["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }] ] }
And we are ready to write some TypeScript code with our fresh pipeline operator
:
const triple = (str: string): string => str + str + str; const kebabify = (str: string): string => str.split('').join('-'); const addQuotes = (str: string): string => `"${str}"`; export const makeDelicous = (str: string): string => str |> triple |> kebabify |> addQuotes;
The compilation with Babel goes successfully 🥳; however, TypeScript got a couple of errors:
src/index.ts:5:59 - error TS1109: Expression expected. ...
The holy moly
So what? Cannot we use the syntax plugin along with type checking goodness and emitting declaration files?…
Begin again
The key to solving this issue is to change the approach. Instead of compiling source files with Babel down to the desired ES version. We’ll use syntax-only plugins:
npm i --save-dev @babel/plugin-syntax-class-properties @babel/plugin-syntax-object-rest-spread @babel/plugin-syntax-typescript
Those plugins do not
compile
the syntax. They justparse
it.
Add the following command to scripts
in .package.json
:
"build:babel": "babel ./src --out-dir lib --extensions \".ts,.tsx\"",
and type in terminal:
npm run build:babel
After this struggle, output lib/index.js
looks like this:
const triple = (str: string): string => str + str + str const kebabify = (str: string): string => str.split('').join('-') const addQuotes = (str: string): string => `"${str}"` export const makeDelicous = (str: string): string => { var _ref, _ref2, _str return ( (_ref = ((_ref2 = ((_str = str), triple(_str))), kebabify(_ref2))), addQuotes(_ref) ) }
Yes, it’s perfectly valid TypeScript!
As of the time of writing this post TypeScript compiler does not support compilation of
.js
files so we’ll need a little hack
Little hack
To allow TypeScript to do what it meant to. We need to change the extension of our output files from .js
to .ts
. So we’ll write a simple Node script:
const fs = require('fs') const path = require('path') // An argument passed when calling script const dir = process.argv[2] if (!dir) throw Error('No directory specified!') const dirPath = path.join('./', dir) fs.readdir(dirPath, (err, files) => { if (err) throw err files.forEach(file => rename(path.join(dirPath, file))) }) const rename = file => { // Matches filenames ending with '.js' if (file.match(/.*\.js$/)) fs.rename(file, `${file.slice(0, -2)}ts`, err => { if (err) throw err console.log(`Renamed ${file}`) }) }
Save it as rename.js
then run:
node rename lib
You should see file extension of /lib/index.js
changed to .ts
Time for TypeScript to rock!
Change your tsconfing.json
to match this:
{ "compilerOptions": { "target": "es5", "lib": ["ESNext"], // Search under node_modules for non-relative imports. "moduleResolution": "node", // Enable strictest settings like strictNullChecks & noImplicitAny. "strict": true, // Disallow features that require cross-file information for emit. "esModuleInterop": true, // Specify output directory to /dist "outDir": "dist", "declaration": true }, // Process files from /lib "include": ["lib"] }
Run
tsc
TypeScript processed file successfully!
Linting
You can use ESlint to lint ts
files. Install eslint
, babel-eslint
and @babel/eslint-plugin
:
npm i --save-dev eslint babel-eslint @babel/eslint-plugin
Create a simple .eslintrc
file in the project root directory:
{ "parser": "babel-eslint" }
Additionaly you can install @typescript-eslint/eslint-plugin
for TypeScript specifc rules:
npm i --save-dev @typescript-eslint/eslint-plugin
And update .eslintrc
:
{ "parser": "babel-eslint", "plugins": ["@babel"], "extends": ["plugin:@typescript-eslint/recommended", "eslint:recommended"] }
Putting it all together
Make sure to have script
for everything in package.json
:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint --ext .ts,.js src/", "build:babel": "babel ./src --out-dir lib --extensions \".ts,.tsx\"", "build:ts": "tsc", "build": "npm run lint & npm run build:babel && node rename lib && npm run build:ts" },
Editor support
That’s clear that editor support for such a quirk is poor out of the box. If you’re using VSCode and wanna get rid of typescript errors; create .vscode
directory on the top of your project and put settings.json
file in with the following content:
{ // This makes ts files are recognized as javascript ones: "files.associations": { "*.ts": "javascript" }, "javascript.validate.enable": false }
Use ESlint plugin To highlight linting errors. For formatting, I use Prettier plugin. Prettier needs a little configuration to work properly in this circumstance. That is the .prettierrc
file:
{ "overrides": [ { "files": "*.ts", "options": { "parser": "babel" } } ] }
Now Prettier will know how to parse our insane ts
files. Time to enjoy:
Caveats and sum up
I don’t find it really useful; therefore, there’s such a strange, almost exotic feeling about this. It’s like breaking some laws in exchange for little dirty pleasure. The type-check errors ain’t pretty neat, cause they show us code snippets pre-compiled with Babel for example:
lib/index.ts:10:3 - error TS2322: Type 'string' is not assignable to type 'number'. 10 return _ref = (_ref2 = (_str = str, triple(_str)), kebabify(_ref2)), addQuotes(_ref);
but that’s only for non-typescript syntax.
What other features do you miss in JavaScript/TypeScript?