Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Migrate Node.js project to TypeScript from plain ES6

Is started migrating a Node.js project from plain ES6 to TypeScript.

What I did:

npm install -g typescript
npm install @types/node --save-dev

Setup tsconfig.json:

{
     "compilerOptions": {
         "emitDecoratorMetadata": true,
         "experimentalDecorators": true,
         "moduleResolution": "node",
         "module": "commonjs",
         "target": "es6",
         "sourceMap": true,
         "outDir": "dist",
         "allowJs": true,
         "forceConsistentCasingInFileNames": true
     },
     "exclude": [
         "node_modules",
         "dist",
         "docs"
     ]
}

Change all file extensions from .js to .ts (except in node_modules):

find . -not \( -path node_modules -prune \) -iname "*.js" -exec bash -c 'mv "$1" "${1%.js}".ts' - '{}' \;

Running tsc now leads to a ton of errors like these:

server.ts:13:7 - error TS2451: Cannot redeclare block-scoped variable 'async'.

13 const async = require('async');
     ~~~~~

Or these:

bootstrap/index.ts:8:7
8 const async = require('async');
        ~~~~~
'async' was also declared here.

Update:

The same happens for retry and other npm packages:

const retry = require('retry');

Changing require statements to ES6 import statements mostly fixed these but having to migrate a few thousands files at once is not feasable so I need a way to stick with require for a while. Is this possible?

like image 276
Alexander Zeitler Avatar asked Jan 10 '19 21:01

Alexander Zeitler


3 Answers

It's possible, but you'll still have to edit those files.

Either of those methods will be enough.

  1. Replace const ... = require() with import ... = require():

    import async = require('async');
    ...
    
  2. Add export {} to the top of the file:

    export {};
    const async = require('async');
    ...
    

The reason of initial issue is that in TS different files are not modules unless they explicitly declared as modules, thus they are compiled/executed in the same global scope, and that's why tsc is reporting you that async variable can't be redeclared.

From documentation:

In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module. Conversely, a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).

like image 97
Styx Avatar answered Sep 24 '22 00:09

Styx


This is the same problem as this one.

In order to be treated as ES module, a file should contain either import or export statement, otherwise a variable is considered to be declared in global scope by TypeScript compiler (even if this is not so at runtime).

The solution is same as in linked question, to add dummy export {}. This could be done in batch with regex replacement but in case CommonJS , module.exports = ... exports are already in use, there may be a conflict between them.

The use of CommonJS require() imports results in untyped code. All major libraries already have according @types/... or built-in typings. Existing NPM packages can be matched with a regex from code base in order to install relevant @types/... packages in batch, imports like const async = require('async') can be replaced in batch with import async from 'async'. This requires esModuleInterop and allowSyntheticDefaultImports options to be set.

like image 34
Estus Flask Avatar answered Sep 24 '22 00:09

Estus Flask


async is a protected keyword. When you use async/await you might skip the 'async' package. If you made ES6+ properly with ECMAScript modules (ESM) you also renamed all your files *.mjs, for example index.mjs. If you have the filename index.js it is most often assumed NOT to be ESM. You have to add types / interfaces to all your ES6 code, so depending on your case it might not be feasible to make all at once, that's why I give the example in ES2015+ ESM notation.

For TypeScript you should be able to use ESM because I guess you want more up to date notation. In order to use async at top level, the async function exist for doing that. Example code for index.mjs that include ES2015+ import from ES5/CommonJS *.js with module.exports and ESM import/export and finally dynamic import:

import { createRequireFromPath } from 'module'; // ESM import
import { fileURLToPath } from 'url';
const require = createRequireFromPath(fileURLToPath(import.meta.url));
// const untypedAsync = require('async');

class Index {

  constructor() {
    this._server = null;
    this.host = `localhost`;
    this.port = 8080;
  }

  set server(value) { this._server = value; }
  get server() { return this._server; }

  async start() {
    const http = await import(`http`); // dynamic import
    this.server = http.createServer(this.handleRequest);
    this.server.on(`error`, (err) => {
        console.error(`start error:`, err);
    });
    this.server.on(`clientError`, (err, socket) => {
        console.error(`start clientError:`, err);
        if (socket.writable) {
            return socket.end(`HTTP/1.1 400 Bad Request\r\n\r\n`);
        }
        socket.destroy();
    });
    this.server.on(`connection`, (socket) => {
      const arrival = new Date().toJSON();
      const ip = socket.remoteAddress;
      const port = socket.localPort;
      console.log(`Request from IP-Address ${ip} and source port ${port} at ${arrival}`);
    });
    this.server.listen(this.port, this.host, () => {
      console.log(`http server listening at ${this.host}:${this.port}`);
    });
  }

  handleRequest(req, res) {
    console.log(`url:`, req.url);
    res.setHeader(`Content-Type`, `application/json`);
    res.writeHead(200);
    res.end(JSON.stringify({ url: req.url }));
  }
}

export default Index; // ESM export
export const randomName = new Index(); // Usage: import { randomName } from './index.mjs';

async function main() {
  const index = new Index();
  const cjs = require(`./otherfile.js`); // ES5/CommonJS import
  console.log(`otherfile:`, cjs);
  // 'async' can be used by using: cjs.untypedAsync
  await index.start();
}

main();

// in otherfile.js
const untypedAsync = require('async');
const test = {
  url: "url test",
  title: "title test",
};
module.exports = { test, untypedAsync }; // ES5/CommonJS export.

However, to use .mjs with typescript currently have some issues. Please look at the related typescript issues that are still open: .mjs input files and .mjs output files. You should at least transpile your .ts to .mjs to solve your problems. The scripts might look like (es6 to ts source):

// in package.json
"files": [ "dist" ],
"main": "dist/index",
"types": "dist/index.d.ts",
"scripts": {
    "mjs": "tsc -d && mv dist/index.js dist/index.mjs",
    "cjs": "tsc -m commonjs",
    "start": "node --no-warnings --experimental-modules ./dist/index.mjs"
    "build": "npm run mjs && npm run cjs"
},
"devDependencies": {
    "typescript": "^3.2.2"
}

// in tsconfig.json
"compilerOptions": {
    "module": "es2015",
    "target": "ES2017",
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": false,
    "strict": true
}
like image 23
Gillsoft AB Avatar answered Sep 27 '22 00:09

Gillsoft AB