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 just parse 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:

Editor Support

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?

Share this article on:

Comment on