How to reference externally defined node modules with string identifiers with typescript - node.js

I'm writing a set of node helper modules in typescript. I'm having difficulty getting typescript to interpret type information for external node modules such as "fs" and "path".
Importantly, I want to separate my module into a bunch of Typescript files, with a class/interface per file. They file layout is like this:
ts/ISomeInterface1.ts
ts/ISomeInterface2.ts
ts/SomeClass1.ts
ts/SomeClass2.ts
A class instantiates one or more interfaces, and is written as follows:
///<reference path="IFileSystemHelpers.ts" />
var fs = require("fs");
namespace myNmspace {
export class SomeClass1 implements SomeInterface1 {
public someIFunction() {
//do work
}
}
}
I'm using gulp-typescript to install type declarations for NodeJs. And I use a tsconfig.json file to build and reference these external typings. Here's a snippet:
{
"version": "1.8.9",
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"preserveConstEnums": true,
"declaration": true,
"suppressImplicitAnyIndexErrors": true,
"out": "./outputfile.js"
},
"filesGlob": [
"./**/*.ts",
"!./node_modules/**/*.ts"
],
"files": [
"./typings/main.d.ts",
"./ts/ISomeInterface1.ts",
"./ts/ISomeInterface2.ts",
"./ts/SomeClass1.ts",
"./ts/SomeClass2.ts",
"./ts/exports.ts"
]
}
Then classes are exported in the exports.ts file:
declare var exports: any;
if (exports) {
exports.SomeClass1 = myNmspace.SomeClass1;
exports.SomeClass2 = myNmspace.SomeClass2;
}
Then comes my problem. How do I get type information for the "fs" module?
I can see the following in the node.d.ts file (see here)that typings has installed:
declare module "fs" {
import * as stream from "stream";
import * as events from "events";
...
}
How do I force Typescript to interpret my fs variable in the SomeClass1.ts file as strongly-typed? In other words, what do I write here:
var fs : ??? = require("fs");
Can anyone help?
As an aside, I've noticed that if I replace the var with an import keyword, I get correct type interpretation for the fs variable. However the terms which point to my interfaces break and I get a squiggly line under the implements ISomeInterface1. Changing the pattern to use imports breaks my file separation, and seems valid only if I want to create a single-file node module.

Use ES6 style imports
import * as fs from 'fs'
import * as path from 'path'
This will also import the definitions from the definition file.
The syntax var x = require('x') does not (however import x = require('x') does, to add to the confusion)

Related

Mongoose Schema pagination typing using mongoose-paginate-ts

EDIT FIXED :
What I onited say in my initial post is that I was exporting the mongoose-pagination-ts function & interface from a global file, and then importing it in my schema files to use it. However, it appears that I need to import the module directly in each schema file in order for it to work, Interface cannot be export in a file and import in another, comming from a node_module in CommonJS (Apparently, not sure if it's the correct reason)
I'm facing the following issue :
The module mongoose-pagination-ts explains as follow :
import { mongoosePagination, Pagination } from "mongoose-paginate-ts";
type User = mongoose.Document & {
username: String,
accounts: [mongoose.Types.ObjectId]
};
const userSchema = new Schema({
username: String,
accounts: [{ type: ObjectId, ref: "Account" }]
});
userSchema.plugin(mongoosePagination);
const User: Pagination<User> = mongoose.model<User, Pagination<User>>("User", userSchema);
//User.paginate()
The build passes with no issue, but when I execute the server in development mode using the following command I have the bellow issue :
nodemon --ext 'ts,js,cjs,json' --ignore 'tests/' --exec 'ts-node-esm src/index.ts'
import { mongoosePagination, Pagination } from "mongoose-paginate-ts";
^^^^^^^^^^
SyntaxError: Named export 'Pagination' not found. The requested module 'mongoose-paginate-ts' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from 'mongoose-paginate-ts';
const { mongoosePagination, Pagination } = pkg;
at ModuleJob._instantiate (node:internal/modules/esm/module_job:124:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:190:5)
tsconfig.js
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "ESNext" /* Specify what module code is generated. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */,
"baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */,
"outDir": "build",
"types": ["node", "mocha"] /* Specify type package names to be included without being referenced in a source file. */,
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": false /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"resolveJsonModule": true
},
"ts-node": {
"swc": true
},
"exclude": ["tests", "node_modules"]
}
Any solution is welcomed, thanks !
The error is clear : the module is exported as a CommonJS module and the proposed solution should work, however, the Pagination interface cannot be destructured as explained :
import pkg from 'mongoose-paginate-ts';
const { mongoosePagination, Pagination } = pkg; // Pagination is not found

"Could not dynamically require 'binding.node'" when trying to rollup speaker module

I'm trying to use rollup to bundle the speaker module.
Once it builds and I try to run it, I get the following error:
Error: Could not dynamically require "/path/to/Project/build/binding.node". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of #rollup/plugin-commonjs appropriately for this require call to work.
From the build:
const os = require$$0__default$3["default"];
const debug = src.exports('speaker');
const binding = bindings.exports('binding'); // Error occurs here
I've tried to add the path and version of it to dynamicRequireTargets with no luck. Thing is, there is no Project/build folder, so I'm not sure if it only exists during build time or if it's a fake path.
How can I get this to build and run correctly?
Minimal reproducible example
$ npm init
$ npm i --save speaker
src/index.ts
import * as Speaker from 'speaker';
console.log(Speaker);
rollup.config.ts
import * as fs from 'fs';
import path from 'path';
import executable from 'rollup-plugin-executable';
import commonjs from '#rollup/plugin-commonjs';
import json from '#rollup/plugin-json';
import { nodeResolve } from '#rollup/plugin-node-resolve';
import typescript from '#rollup/plugin-typescript';
const incrementalDependencyLoader = {
input: 'src/index.ts',
output: {
file:
'/dist/' +
require(path.join(process.cwd(), 'package.json')).name,
// cjs translates to CommonJs which is supported by Node
format: 'cjs',
banner: '#!/usr/bin/env node\n',
},
plugins: [
nodeResolve(),
typescript({
tsconfig: fs.existsSync(path.join(process.cwd(), './tsconfig.build.json'))
? './tsconfig.build.json'
: './tsconfig.json',
}),
json(),
commonjs({}),
executable(),
],
external: [],
};
// with using an array, we can create multiple bundled javascript files
export default [incrementalDependencyLoader];
tsconfig.json
{
"compilerOptions": {
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "node",
"target": "esnext",
"lib": ["esnext"],
"baseUrl": ".",
"allowSyntheticDefaultImports": true
}
}

Configure typescript to output imports with extensions, ex: `from './file.js``

Using node 14.x I would like to switch my project to full ES Modules, as it's now supported.
So I enabled on package.json "type": "module"
and my tsconfig.json looks like that:
{
"compilerOptions": {
"outDir": "dist", /* Redirect output structure to the directory. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"allowSyntheticDefaultImports": true,
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "node",
"target": "ES2020"
}
}
But the output files have unspecified extension in imports, it's instead REQUIRED for node 14.x to specify full file ext
For example:
import { ENV, redisConfig } from './config';
should instead be:
import { ENV, redisConfig } from './config.js';
TS won't handle that for you, but you can run Node with the node --experimental-specifier-resolution=node parameter.
Source
Unfortunately it seems that TS isn't going to support adding extension to the end of the import, since apparently it's not as simple as adding .js to the end of the import.

Typescript declaration file for an external npm package - constructor

I am using ES6 and Typescript for my Node project, however one library is a commonjs library.
For that library, I created my own .d.ts declaration file:
module "#alpacahq/alpaca-trade-api" {
export interface AlpacaParams { ... }
// ...
export class Alpaca implements Broker {
// ...
constructor(params: AlpacaParams);
}
export default Alpaca;
}
Everything works as expected, but I'm having a problem with the constructor.
If I use that class from within my project, and I try this:
this.alpaca = new Alpaca.Alpaca({...});
I get told that Alpaca.Alpaca is not a constructor.
The only way it seems to work is if I do:
this.alpaca = new Alpaca.default({...});
I'm quite new to Typescript, so I'm sure I'm doing something wrong. Any ideas?
The latter works, so I'm not blocked in my work, but I would like to set things up properly.
Thank you!
Edited to show TS config and imports
tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"lib": ["es6", "es5"],
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"moduleResolution": "node",
"typeRoots": ["./types"],
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
This is how I import it. Couldn't figure out how to make it work otherwise. If I import modules ES6 style, it breaks unless I use commonjs. If I use commonjs, I get an "export undefined" error.
import * as Alpaca from '#alpacahq/alpaca-trade-api';
The problem is that you're importing all named exports with import * meaning that Alpaca refers to the module and .default refers to the exported class. Instead you should be importing just the default member.
Change your import to look like this:
// Import default member from module
import Alpaca from '#alpacahq/alpaca-trade-api';
this.alpaca = new Alpaca({...});

ReferenceError: Node is not defined (trying to use Node interface in typescript function in nodejs application)

While extending Cheerio library, I implemented the following static function (other extension functions work fine):
$.nodeType = function (elem: CheerioElement): number {
switch (elem.type) {
case "comment":
return Node.COMMENT_NODE; // <--- it fails here
case "tag":
return Node.ELEMENT_NODE; // <--- it fails here
case "text":
return Node.TEXT_NODE; // <--- it fails here
default:
return -1;
}
};
The following error appears during runtime (compilation with tsc -b succeed):
ReferenceError: Node is not defined
Node interface is part of the DOM API. Thus, I realized the need of explicitly include the DOM API under compilerOptions section of tsconfig.json.
However, I still get that runtime error.
Minimal relevant part of tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"incremental": true,
"lib": [
"esnext",
"dom"
],
"module": "commonjs",
"noImplicitAny": true,
"outDir": "./lib/",
"sourceMap": true,
"target": "esnext",
"watch": true
},
"include": [
"./src/**/*.ts",
]
}
I thought of explicitly import Node lib in the specific .ts file which contains the function, but I didn't find any "include-able" standard DOM lib.
Including a typescript lib doesn't polyfill the feature in an environment where it is not available.
While including "dom" type definitions will make the types available (at the compile time), but it doesn't actually make the Node API (typically provided by the browser runtime) available at runtime.
If you really need this functionality at runtime, you will need to also include an implementation of DOM for node.js such as jsdom which provides this API.
lorefnon explained the problem; here's a slightly hacky way to fix it:
Install jsdom
npm install jsdom
Add this to the top of your file:
const { JSDOM } = require('jsdom'); // or import { JSDOM } from 'jsdom';
const Node = new JSDOM('').window.Node;

Resources