将 Node.js 个项目从纯 ES6 迁移到 TypeScript

Migrate Node.js project to TypeScript from plain ES6

已开始将 Node.js 项目从纯 ES6 迁移到 TypeScript。

我做了什么:

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

设置 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"
     ]
}

将所有文件扩展名从 .js 更改为 .tsnode_modules 除外):

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

运行 tsc 现在会导致大量错误,例如:

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

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

或者这些:

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

更新:

retry 和其他 npm 软件包也是如此:

const retry = require('retry');

require 语句更改为 ES6 import 语句主要解决了这些问题,但是必须一次迁移几千个文件是不可行的,所以我需要一种方法来坚持使用 require一会儿。这可能吗?

可以,但您仍然需要编辑这些文件。

这些方法中的任何一种都足够了。

  1. const ... = require()替换为import ... = require():

    import async = require('async');
    ...
    
  2. export {}添加到文件顶部:

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

初始问题的原因是在 TS 中不同的文件是 不是 模块,除非它们明确声明为模块,因此它们在同一全局范围内 compiled/executed,这就是 tsc 报告您 async 变量不能 重新声明 .

的原因

来自 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).

这与 的问题相同。

为了被视为 ES 模块,文件应包含 importexport 语句,否则变量将被 TypeScript 编译器视为在全局范围内声明(即使这个在运行时不是这样。

解决方案与链接问题中的相同,添加虚拟 export {}。这可以通过替换正则表达式来批量完成,但如果 CommonJS,module.exports = ... 导出已经在使用中,它们之间可能会发生冲突。

使用 CommonJS require() 导入会产生非类型化代码。所有主要图书馆都已经根据 @types/... 或内置打字。现有的 NPM 包可以与代码库中的正则表达式匹配,以便批量安装相关的 @types/... 包,导入 const async = require('async') 可以批量替换为 import async from 'async'。这需要设置 esModuleInteropallowSyntheticDefaultImports 选项。

async 是受保护的关键字。当您使用 async/await 时,您可能会跳过 'async' 包。如果您使用 ECMAScript 模块 (ESM) 正确地制作了 ES6+,您还重命名了所有文件 *.mjs,例如 index.mjs。如果您的文件名是 index.js,则通常认为它不是 ESM。您必须向所有 ES6 代码添加类型/接口,因此根据您的情况,一次性完成所有操作可能不可行,这就是我以 ES2015+ ESM 表示法给出示例的原因。

对于 TypeScript,您应该能够使用 ESM,因为我猜您需要更新的符号。为了在顶层使用异步,async 函数 就是为此而存在的。 index.mjs 的示例代码包括从 ES5/CommonJS *.js 和 module.exports 和 ESM import/export 导入的 ES2015+ 以及最后的动态导入:

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.

但是,将 .mjs 与打字稿一起使用目前存在一些问题。请查看仍未解决的相关打字稿问题:.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
}

由于您要将一个大型项目迁移到 TypeScript,我建议使用一些工具,例如这个包 (https://www.npmjs.com/package/javascript-to-typescript),它可以自动完成一些工作。

您可以编写一个脚本来打开项目中的每个文件,并按照@Styx 在他的回答中的建议在顶部添加 export {}