Migrate Node.js project to TypeScript from plain ES6 - node.js

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?

It's possible, but you'll still have to edit those files.
Either of those methods will be enough.
Replace const ... = require() with import ... = require():
import async = require('async');
...
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).

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.

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
}

Since you are migrating a large project to TypeScript, I would suggest using some tool like this package (https://www.npmjs.com/package/javascript-to-typescript) which could automate some of the work.
You can write a script to open each file in the project and add export {} at the top as suggested by #Styx in his answer.

Related

#path not resolving when using Jest

import { Utils } from '#domain/Utils'
import '#testing-library/jest-dom'
import useHasAccess from './useHasAccess'
describe('tests', () => {
it('should have access', () => {
const isHasAccess= useHasAccess()
const isAdmin = isHasAccess(Utils.Identity.Admin)
expect(isAdmin).toBe(true);
})
})
When running this simple test, I get: Cannot find module '#domain/Utils'
I tried putting the relative path, but I get the same error, but coming from a different file referenced by the imports, so I am wondering if there's a way for Jest to recognize the path settings set by Typescript. ESLint seems to recognize the path, so I am thinking something prevents Jest from finding the path.
"paths": {
"#domain/*": [
"./src/domain/*"
]
},
I found this inside my tsconfig, but there's no place to put it inside the jest configs:
module.exports = {
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{js,jsx}'],
coverageDirectory: 'coverage',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/custom.setup.js'],
}

Sequelize CLI & DB migrations with Typescript

Sequelize docs claim that it works with Typescript but in order for it to be useful in production one needs to use DB migration scripts. These can only be executed using the Sequelize CLI but this CLI seems to have no regard for Typescript at all. It only generates (and apparently runs) JS files. There is a "sequelize-cli-typescript" project on NPM but it's 4 years old! And the only answer on Stack Overflow to a related question is literally 6 words long, none of them useful: Using sequelize cli with typescript
My setup is like so:
I created a basic Node app with Typescript and a simple tsconfig.js like so:
{
"compilerOptions": {
"allowJs": true,
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*"
]
}
},
"include": [
"*"
]
}
I'm also using ESLint with the following .eslintrc:
{
"root": true,
"parser": "#typescript-eslint/parser",
"plugins": [
"#typescript-eslint"
],
"extends": [
"airbnb-base", "airbnb-typescript/base"
],
"parserOptions": {
"project": "./tsconfig.json"
}
}
I then tried running the Sequelize CLI init command, npx sequelize-cli init as described in the DB migrations documentation. This caused a file models/index.js to be created with the following content:
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
This causes my IDE (VS Code) to show an error:
Parsing error: "parserOptions.project" has been set for #typescript-eslint/parser.
The file does not match your project config: models/index.js.
The file must be included in at least one of the projects provided.
I have no idea what this is supposed to mean. I've read numerous explanations and none makes sense to me. I have allowJs set to true in tsconfig and include all. What am I missing? More importantly how can I use Typescript for DB migrations and CLI generated code?
As for generated code for connecting to DB and registering models you should convert it manually to ts (a one-time thing usually).
As for migrations I use the following workaround:
configure migrations path in .sequelizerc to a subfolder in the folder where the compiled app will be placed by TS (it's important in order to be able to apply migrations on testing and prod environments), something like:
const { resolve } = require('path');
module.exports = {
config: resolve('build/application/config.js'),
'seeders-path': resolve('build/application/seeders'),
'migrations-path': resolve('build/application/migrations'),
'models-path': resolve('application/models')
};
generate a migration file with a given name:
sequelize migration:create --name add-some-table
move the generated file to application/migrations and change its extension to .ts
replace the content of the file with some kind of a migration Typescript-template:
import { QueryInterface, DataTypes, QueryTypes } from 'sequelize';
module.exports = {
up: (queryInterface: QueryInterface): Promise<void> => queryInterface.sequelize.transaction(
async (transaction) => {
// here go all migration changes
}
),
down: (queryInterface: QueryInterface): Promise<void> => queryInterface.sequelize.transaction(
async (transaction) => {
// here go all migration undo changes
}
)
};
add necessary changes to the migration file
Basically, it's possible to write some script file to simply generate .ts migration file copying a predefined template file in application/migrations folder directly with a given name to combine steps 2-4 into a single step.
If you will come up with a better solution eventually feel free to share it in the comments or even as a separate answer here.

Typescript picking namespace instead of function while import

I am working on a project in which i wanted to generate typings from graphql using gql2ts library. In the gql-2-ts file i'm using a namespace import for glob , but typescript is showing me error which is intended. I then changed the import to default import but still it is picking the namespace object instead of function.
gql-2-ts.ts
import fromQuery from '#gql2ts/from-query'
import DEFAULT_OPTIONS from '#gql2ts/language-typescript'
import * as fs from 'fs-extra'
import * as G from 'glob' // namespace import
// import G from 'glob' // default import
import * as graphql from 'graphql'
import * as path from 'path'
import {promisify} from 'util'
const NO_OF_DIRECTORIES_OFFSET = 2
const {Kind} = graphql
const glob = promisify(G)
const readFile = promisify<string, Buffer>(fs.readFile)
const writeFile = <(file: string, content: string) => void>(
promisify(fs.writeFile)
)
const removeWhiteSpace = (q: string) =>
q.replace(/\n/g, '').replace(/\s\s*/g, ' ')
const identity = <T>(arg: T): T => arg
const main = async () => {
const {data} = await fs.readJSON('graphql.schema.json')
const args = process.argv.slice(2)
for (let i = 0; i < args.length; i++) {
const cwd = path.resolve(process.cwd(), args[i])
const files = await glob('**/*.graphql', {
cwd: cwd
})
const allEnumsArray = await Promise.all(
files
.map((i) => path.resolve(cwd, i))
.map((path) => compileFile(path, data, args[i]))
)
const enumsData: EnumData = {}
allEnumsArray.forEach((enumsMap) => {
if (enumsMap) {
Object.keys(enumsMap).forEach((enumName) => {
enumsData[enumName] = enumsMap[enumName]
})
}
})
const allEnums = Object.keys(enumsData)
await Promise.all(
allEnums.map((enumName) =>
writeEnumsFile(enumName, enumsData[enumName], args[i])
)
)
console.log(`Compiled ${files.length} files inside ${args[i]}`) // tslint:disable-line
}
}
main().catch((err) => {
console.error(err) // tslint:disable-line
process.exit(1)
})
this is my tsconfig file.
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"skipLibCheck": true,
"jsx": "react",
"esModuleInterop": true,
"downlevelIteration": true,
"allowSyntheticDefaultImports": true,
"lib": [
"dom", "dom.iterable"
]
},
"exclude": ["node_modules"]
}
I'm getting the below error in case of namespace import
Type originates at this import. A namespace-style import cannot be called or constructed, and will cause a failure at runtime. Consider using a default import or import require here instead. *
and while using the default import
. from_query_1.default(...).map is not a function
because it picked the namespace object .
I found one doc related to it , but that didn't helped much.
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html
Note: The new behavior is added under a flag to avoid unwarranted breaks to existing code bases. We highly recommend applying it both to new and existing projects. For existing projects, namespace imports (import * as express from "express"; express();) will need to be converted to default imports (import express from "express"; express();).
I figured out the problem. There was version conflict in some of the libraries .
Use these versions for the following libraries
"#gql2ts/from-query": "^1.9.0",
"#gql2ts/language-typescript": "^1.9.0"
and use default import, it will work . Keeping the post in case someone bumps into the same error.

TypeError: Reflect.hasOwnMetadata is not a function

I am trying to use inversify with typescript and node.js. I am currently using node.js version 6.9.1, typescript version 2.6.2, and ECMAScript 6. When I try to run the node.js application, I keep receiving the following error, "TypeError: Reflect.hasOwnMetadata is not a function".
Following documentation I found online, by adding import "reflect-matadata" at the top of the inversify.config.js file, it seemed to work for awhile, but now the problem is back again.
In my inversify.config.ts file, I have the following code that gets called from my app.ts file:
export let initialize = async (): Promise<Container> => {
var container = new Container();
try {
if (!Reflect.hasOwnMetadata("inversify:paramtypes", ClassToAddInjectableTo)) {
decorate(injectable(), ClassToAddInjectableTo);
}
….
container.bind<interfaces.Controller>(TYPE.Controller).to(MyController1).whenTargetNamed('MyController1');
container.bind<interfaces.Controller>(TYPE.Controller).to(MyController2).whenTargetNamed('MyController2');
} catch (ex) {
throw ex;
}
}
This code exists in app.ts and it calls the code in the inversify.config.ts file:
setup = async () => {
try {
let container: Container = await initialize();
…..
} catch (err){
throw err;
}
}
The problem seems to be on the following line in node_modules/inversify/lib/annotation/decorator_utils.js:22:
if (Reflect.hasOwnMetadata(metadataKey, annotationTarget) === true) {
In the generated inversify.config.js file, the following code seems to be above the import "reflect-metadata":
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
require("reflect-metadata");
I noticed that the help I found online only indicated that I needed to add import "reflect-metadata".
Also, here is my tsconfig.json file:
{
"compilerOptions": {
"module": "commonjs",
"target": "ES6",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"types": ["reflect-metadata"],
"lib": ["ES6"],
"sourceMap": true,
"inlineSources": true,
"pretty": true,
"outDir": "dist",
"rootDir": "src",
"noLib": false,
"declaration": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
What could I be missing?
Update
For the issue above, I added the import "reflect-metadata" to the file inversify.config.ts file. However, I commented this import statement and added the import "reflect-metadata" to the app.ts file and now I am getting a different error:
throw new Error(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);
^
Error: Cannot apply #injectable decorator multiple times.
Now I found a post on the internet that seemed to describe indicate the adding import "reflect-metadata" adds a Reflect global variable. Also, I don't know if this helps but I removed the #injectable decorator from the controllers.
Try:
npm install reflect-metadata --save
Then import it only once in your entire application:
import "reflect-metadata"
If you are using inversify-express-utils make sure that your controllers are annotated with #controller not with #injectable. Also make sure that your coontrollers are imported once.
import "./controllers/some_controller";
import "./controllers/another_controller";
In case of failure during running jest test, add to the top of the spec file:
import "reflect-metadata";

typescript error running navalia example

I am trying to run this example from https://github.com/joelgriffith/navalia but for the light of me, I couldn't get it to work without error:
navaliatest.ts
/// <reference path="typings.d.ts" />
import { Chrome } from 'navalia';
const chrome = new Chrome();
async function buyItOnAmazon() {
const url = await chrome.goto('https://amazon.com');
const typed = await chrome.type('input', 'Kindle');
const clicked = await chrome.click('.nav-search-submit input');
chrome.done();
console.log(url, typed, clicked); // 'https://www.amazon.com/', true, true
}
buyItOnAmazon();
tsconfig.json
{
"files": [
"navaliatest.ts"
],
"compilerOptions": {
"noImplicitAny": false,
"target": "es6",
"moduleResolution": "node",
"paths": {
"*" : ["/usr/local/lib/node_modules/*"]
}
}
}
typings.d.ts
/// <reference path="/usr/local/lib/node_modules/navalia/build/Chrome.d.ts" />
declare module 'navalia' {
var Chrome: any;
export = Chrome;
}
Below are the versions:
MacBook-Pro:testcasperjs myusername$ node --version
v6.11.2MacBook-Pro:testcasperjs myusername$ npm --version
3.10.10
MacBook-Pro:testcasperjs myusername$ tsc --version
Version 2.4.2
This is the error I got although I do get .js file output:
MacBook-Pro:testcasperjs myusername$ tsc navaliatest.ts
../../../usr/local/lib/node_modules/navalia/node_modules/chrome-launcher/chrome-finder.ts(203,16): error TS2339: Property 'from' does not exist on type 'ArrayConstructor'.
../../../usr/local/lib/node_modules/navalia/node_modules/chrome-launcher/chrome-launcher.ts(99,15): error TS1056: Accessors are only available when targeting ECMAScript 5 and higher.
navaliatest.ts(3,10): error TS2305: Module ''navalia'' has no exported member 'Chrome'.
I am sure there is a stupid mistake somewhere but please could someone help me and take a look? Thanks.
You don't need to redeclare navalia. It has already been done for you at node_modules/navalia/build/index.d.ts given that moduleResolution is set to Node
You'll need to set module to commonjs so that you can run it in node
tsconfig.json
{
"files": [
"navaliatest.ts"
],
"compilerOptions": {
"noImplicitAny": false,
"target": "es6",
"module": "commonjs",
"moduleResolution": "Node"
}
}
navaliatest.ts (No change)
import { Chrome } from 'navalia';
const chrome = new Chrome();
async function buyItOnAmazon() {
const url = await chrome.goto('https://amazon.com');
const typed = await chrome.type('input', 'Kindle');
const clicked = await chrome.click('.nav-search-submit input');
chrome.done();
console.log(url, typed, clicked); // 'https://www.amazon.com/', true, true
}
buyItOnAmazon();
It'll create navaliatest.js with no errors, which can be run in node.

Resources