Extending TypeScript Global object in node.js - node.js

I have a node.js app that attaches some config information to the global object:
global.myConfig = {
a: 1,
b: 2
}
The TypeScript compiler doesn't like this because the Global type has no object named myConfig:
TS2339: Property 'myConfig' does not exist on type 'Global'.
I don't want to do this:
global['myConfig'] = { ... }
How do I either extend the Global type to contain myConfig or just tell TypeScript to shut up and trust me? I'd prefer the first one.
I don't want to change the declarations inside node.d.ts. I saw this SO post and tried this:
declare module NodeJS {
interface Global {
myConfig: any
}
}
as a way to extend the existing Global interface, but it doesn't seem to have any effect.

As of node#16 the NodeJS.Global interface has been removed in favor of globalThis.
You can declare new global variable in a module file as:
declare global {
var NEW_GLOBAL: string;
}
And in a non-module file (no top-level import/export) as:
declare var NEW_GLOBAL: string;
Important note: variable must be declared as var. let and const variables don't show up on globalThis.

I saw this SO post and tried this:
You probably have something like vendor.d.ts:
// some import
// AND/OR some export
declare module NodeJS {
interface Global {
spotConfig: any
}
}
Your file needs to be clean of any root level import or exports. That would turn the file into a module and disconnect it from the global type declaration namespace.
More : https://basarat.gitbooks.io/typescript/content/docs/project/modules.html

To avoid Typescript claim something like this:
TS2339: Property 'myConfig' does not exist on type 'Global'.
I suggest to define custom types. I do it under src/types/custom.d.ts file in my project:
declare global {
namespace NodeJS {
interface Global {
myConfig: {
a: number;
b: number;
}
}
}
}
Then I ensure these are considered by Typescript in tsconfig.json file:
{
...
"files": [
...
"src/types/custom.d.ts"
]
}
Now you're safe to use your custom property:
console.log(global.myConfig.a);

Putting the following file into our project's root directory worked.
global.d.ts
declare namespace NodeJS {
export interface Global {
myConfig: any
}
}
We're using "#types/node": "^7.0.18" and TypeScript Version 2.3.4. Our tsconfig.json file looks like this:
{
"compilerOptions": {
"module": "commonjs",
"target": "es6"
},
"exclude": [
"node_modules"
]
}

Use 'namespace' instead of 'module' to declare custom TypeScript
If you use any of the above answers and are using a newer version of Typescript you'll get a nag about using "module". You should consider namespace instead.
In order to satisfy the requirement here you'll actually need more than extending the Global interface. You'll also need to create a constant with the type as well if you want it to be accessible directly from the "globalThis" context.
NOTE: while the OP asked about an object literal the process is the same as you see here below. Rather than the "Debug" type being a function you would simply define the interface as need, then change "debug:" to myConfig or whatever you wish.
// typically I'll store the below in something like "typings.d.ts"
// this is because, at least typically, these overrides tend to
// be minimal in nature. You could break them up and Typescript
// will pick them up if you wish.
// Augmentations for the global scope can only be directly nested
// in external modules or ambient module declarations.
export {}
declare global {
// Definition or type for the function.
type Debug = (label: string) => (message: any, ...args: any[]) => void
// If defining an object you might do something like this
// interface IConfig { a: number, b: number }
// Extend the Global interface for the NodeJS namespace.
namespace NodeJS {
interface Global {
// Reference our above type,
// this allows global.debug to be used anywhere in our code.
debug: Debug
}
}
// This allows us to simply call debug('some_label')('some debug message')
// from anywhere in our code.
const debug: Debug
}
How the above might be used
For completeness in this example all we did was define a global so we can log a simple debug message. Here's how we'd bind the method to our global context.
global.debug = (label: string) => (message: any, ...args: any[]) => console.log(message, ...args)
We can also call our global debug method directly:
debug('express')(`${req.method}: ${req.url}`)

The only thing that works for me is this:
// lib/my.d.ts
import Global = NodeJS.Global;
export interface CDTGlobal extends Global {
cdtProjectRoot: string
}
and then using it in other files like so
import {CDTGlobal} from "../lib/my.d.ts";
declare const global: CDTGlobal;
const cwd = global.cdtProjectRoot; // works

What worked for me is:
declare global {
module NodeJS {
interface Global {
myConfig: any;
}
}
}
global.myConfig = 'it works!';
Only downside is when using it you will have to turn off the ESLint rule #typescript-eslint/no-namespace.
For completeness here is my tsconfig.json:
{
"compilerOptions": {
"declaration": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"lib": ["dom", "es2017"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"removeComments": true,
"resolveJsonModule": true,
"rootDir": "src",
"sourceMap": true,
"strict": true,
"target": "es6"
},
"exclude": ["dist", "node_modules"]
}

Copy my answer of another post:
globalThis is the future.
// Way 1
var abc: number
globalThis.abc = 200 // no error
// Way2
declare var age: number
globalThis.age = 18 // no error

I can get both type check and code intelligence.
declare namespace NodeJS {
interface Global {
property: string
}
}
But interface Global is point to global.GLOBAL.
You can get correct type check, is because:
declare var global: NodeJS.Global & typeof globalThis;
But you can not get better code intelligence by useing global.property, unless use global.GLOBAL.property.
So, you need to define global var global and extend interface Global both:
// myglobal.d.ts
declare namespace NodeJS {
interface Global {
property: string
}
}
declare var global: NodeJS.Global & typeof globalThis
And now you can get property intelligence when you type global.

Related

WebStorm/JetBrains error: Arrays missing native functionality in TypeScript

Edit 3: Moved this to the top since it is relevant to the nature of the question. Per AlexWayne's suggestion, I tried a far simpler code sample, and arrived at the same problem. Given the simpler code, I was able to easily compile it using only the tsc utility, and it compiles cleanly in the terminal. That means this is strictly a WebStorm issue, for who knows what reason. Problem isn't really solved, but at least I can keep working and just know to ignore what WebStorm is saying right now.
Before you ask, yes, I've installed #types/node.
This was in the middle of me rewriting it in an attempt to resolve the issue, so forgive me for the unused variable. It started out as a spread to add to lines, but when I started getting an error saying that IBestPracticeStandard[] didn't have a method map, I changed the type signature to Array<IBestPracticeStandard> which had as little effect as you'd expect. I then changed it to a for(const standard of standards) loop, but then I got an error saying IBestPracticeStandard[] a lacked [Symbol.toIterator]() method. My last attempt was what you see, where I turned it into a classic for(let i = 0; i < standards.length; i++) loop, but this too came up with length does not exist on an Array.
I'm at my wits end on this. I know the code itself works, as I imported it from an existing project written in JavaScript. I've been working on this TypeScript overhaul for months, and this is the first time I've encountered such a bizarre error. Any help would be greatly appreciated.
// ./src/interfaces/IBestPracticeStandard.ts
export interface IBestPracticeStandard {
id: number;
name: string;
}
// ./src/interfaces/IConcern.ts
export interface IConcern extends IConcernObject {
_path: string;
_engineTestId: number;
_attribute: string;
_bestPracticeId: number;
_element: string;
_fixType?: unknown;
_needsReview: boolean,
_rawEngineJsonObject: IConcernObject;
_bestPracticeDescription: string;
_severity: number;
_noticeability: number;
_tractability: number;
_bestPracticeDetailsUrl: string;
_bestPracticeStandards: IBestPracticeStandard[];
rawEngineJsonObject: IConcernObject;
toJSON(): IConcernObject;
}
// ./src/AccessibilityConcernMessage.ts
import { IBestPracticeStandard, IConcern } from './interfaces';
export class AccessibilityConcernMessage {
private readonly score: number;
private readonly message: string;
constructor(concern: IConcern) {
const {
_bestPracticeDescription: description,
_bestPracticeStandards: standards,
_bestPracticeDetailsUrl: url,
_severity: severity,
_noticeability: noticeability,
_tractability: tractability,
_rawEngineJsonObject: {
attribute: attr,
attributeDetail: detail,
path,
fingerprint: { css }
}
} = concern;
const lines: string[] = [
`Attribute: ${attr}`,
`${detail}\nBest Practice: ${description}`,
url
];
for(let i = 0; i < standards.length; i++) {
const { name } = standards[i];
lines.push(`- ${standard.name}`);
}
this.score = (severity + noticeability + tractability) / 30;
this.message = `${lines.join('\n\t')}\nFound At Path: ${path}\nCSS Locator: ${css}`
}
toString(): string {
return this.message;
}
valueOf(): number {
return this.score;
}
}
Edit: Adding to say I'm getting additional TypeScript errors that say string[] doesn't have method push, no index signature on IBestPracticeStandard[]
Edit 2: Adding my tsconfig.json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
"incremental": true, /* Enable incremental compilation */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
/* JavaScript Support */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
/* Interop Constraints */
"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. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
"noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
"noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
It seems, as AlexWayne pointed out, that WebStorm had silently devolved into a bad state. Closing it and reopening the IDE re-initialized all of the internal workings and it is now registering everything properly. Thanks to the quick and helpful comments

Adding types["node"] in tsconfig results in type definition error

I have a NodeJS project that has a type declaration file so I can add properties to the Request object:
#types/express/index.d.ts:
import { User } from "../../src/entity/user.entity";
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
tsconfig.json:
"compilerOptions": {
"typeRoots": ["#types"],
...
}
So far, this works perfectly fine. However, when I want to add types or change typeRoots in my tsconfig.json to:
"compilerOptions": {
"typeRoots": ["./node_modules/#types", "#types"]
"types": ["node"]
...
}
it suddenly does not work anymore and throws errors at me in VSCode:
Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.ts(2339)
Any idea what causes this? Help is appreciated!
In ambient declaration files, you cannot have either import or export, because that would make them of type 'module'.
That means to use the typings defined in the declaration file, you would have to import or reference it in every file you need to use it in.
However, without the import or export, it's a script type file, and works as an ambient declaration file.
So how do we get around this:
import { User } from "../../src/entity/user.entity";
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
Well, it's a little known feature that you can use import like a function in a type:
declare global {
namespace Express {
interface Request {
user?: import("../../src/entity/user.entity").User;
}
}
}

#mailchimp/mailchimp_marketing/types.d.ts' is not a module in nodeJs

I imported import #mailchimp/mailchim_marketing in my NodeJS app:
import mailchimp from "#mailchimp/mailchimp_marketing";
However, it gives following error:
type.d.ts is not a module
I have searched to see if there is a #types/#mailchimp/mailchimp_marketing but I couldn't see it.
The type.d.ts file provided by #mailchimp/mailchimp_marketing doesn't have the types of the library and the package doesn't have a #types package too. So it's necessary to create your own to override the provided by him.
To do this, creates the folders #types/#mailchimp/mailchimp_marketing (one inside another) and creates the file index.d.ts inside mailchimp_marketing.
This file has to contain the declaration of the module, and inside than, the functions and types what you gonna use from library. In my case:
declare module '#mailchimp/mailchimp_marketing' {
type Config = {
apiKey?: string,
accessToken?: string,
server?: string
}
type SetListMemberOptions = {
skipMergeValidation: boolean
}
export type SetListMemberBody = {
email_address: string,
status_if_new: 'subscribed' | 'unsubscribed' | 'cleaned' | 'pending' | 'transactional'
merge_fields?: {[key: string]: any}
}
export default {
setConfig: (config: Config) => {},
lists: {
setListMember: (listId: string, subscriberHash: string, body: SetListMemberBody, opts?: SetListMemberOptions): Promise<void> => {}
}
}
}
SetListMemberBody has much more fields and setListMember is not void, but i added just what I gonna use. To discover this fields and functions I looked in the source code (https://github.com/mailchimp/mailchimp-marketing-node) and api documentation (https://mailchimp.com/developer/api/marketing/list-members/add-or-update-list-member/).
In my case (Typescript 3.7.3) was not necessary to change tsconfig.json, but if you use older version maybe is necessary to add "typeRoots" for your compilerOptions in tsconfig.json:
"compilerOptions": {
"typeRoots": ["#types", "node_modules/#types"]`
// other options
}
After all this, I used the library normally:
import mailchimp, { SetListMemberBody } from '#mailchimp/mailchimp_marketing'
import crypto from 'crypto'
mailchimp.setConfig({
apiKey: process.env.MAILCHIMP_KEY,
server: 'serverHere',
});
const listId = 'listIdHere'
export const addSubscriber = async (member: SetListMemberBody): Promise<void> => {
const hash = crypto.createHash('md5').update(member.email_address).digest('hex')
await mailchimp.lists.setListMember(listId, hash, member)
}
replace your code to this:
const mailchimp = require("#mailchimp/mailchimp_marketing");
Right, you wont have type safe but at least your code will work.
Types for #mailchimp/mailchimp_marketing are available meanwhile.
Use
yarn add -D #types/mailchimp__mailchimp_marketing
or
npm install --save-dev #types/mailchimp__mailchimp_marketing
to install the types package.
EDIT: the types do not seem to be complete.
With a similar issue with #mailchimp/mailchimp_transactional I had to create my own mailchimp__mailchimp_transactional.d.ts with declare module (this package also has just types.d.ts and it is almost empty unlike the types.d.ts in mailchimp_marketing package).
So you can type to create your own type description file using their types.d.ts, place it in #types folder of your project and add #types to tsconfig.json like this:
"compilerOptions": {
// other options here
"typeRoots": [
"#types",
"node_modules/#types"
]
mailchimp__mailchimp_transactional.d.ts
/* eslint-disable camelcase */
declare module '#mailchimp/mailchimp_transactional' {
...
}
A quick & dirty solution is to delete the types.d.ts file, which prevents the error, though you will no longer get any type information for the API.

How to export a TypeScript module so that can be imported both from TypeScript and Node?

Let's say a have the following TypeScript module:
function foo () {
return 123;
}
//TODO: Export code here
I want to export it in such a way that can be imported in these ways from TypeScript:
import foo from 'foo';
import * as foo from 'foo';
and in this way from Node:
const foo = require ( 'foo' );
Requirements:
I don't what the users of my module to have to set the allowSyntheticDefaultImports option
I want the code for exporting the module to be as clean as possible
I want to preserve type definitions
So far I've come up with the following "solutions", but they either don't preserve type definitions well enough or are too verbose:
export = foo['default'] = foo as typeof foo & { default: typeof foo };
export = foo['default'] = foo;
Is there a better way?
This is the best that I could come up with:
export = Object.assign ( foo, { default: foo } );
It's pretty terse, type definitions are properly generated, and it can be imported using all the aforementioned methods.
Since you are publishing as a commonjs package, you don't need rollup/webpack.
All you need is to transpile your code to es5 in commonjs using the TypeScript compiler.
Your tsconfig.json should look like this:
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es5",
"outDir": "dist" // customize this yourself.
...
}
}
And in your package.json:
// package.json
{
"main": "dist/index.js",
"typings": "dist/inde.d.ts",
"files": [
"dist" // customize this yourself.
],
...
}
Here is an example repository you can take a look into:
https://github.com/unional/path-equal
As far as I know export and imports is not supported in NodeJS yet.
Check this article: node-js-syntaxerror-unexpected-token-import
And as far as I know the only way to use TypeScript types syntax is only in a .ts file so an option is to compile your needed .ts files to .js using Babel.

Declare interface in TypeScript .d.ts file and consume from JavaScript file

I'm using Visual Studio Code and would like to have intellisense hint when declaring my configuration variable as plain javascript object
jsconfig.json
{
"compilerOptions": {
"checkJs": true
},
"include": [
"src/**/*.js",
"types/**/*.d.ts"
],
"exclude": [
"node_modules"
]
}
types/index.d.ts
interface Foo {
a: string;
b: number;
}
declare var fooConfig: Foo;
src/app.js
const fooConfig = {
a: 'hello',
b: 123
}
I expect when declaring using const fooConfig VS Code might offer intellisense about a and b, the current result I got complaint message about re-declaring the variable fooConfig
P.S. I don't really know the possibility but I would like to have some intellisense so I can declare my configuration variable easily
Please guide
Thanks
You can use the JSDoc syntax to describe types in JS files:
src/app.js
/** #type {Foo} */
const fooConfig = {
a: 'hello',
b: 123
}
It works implicitly with d.ts files you have in your project, so there is no need to "include" them and there is no need to declare the var in the d.ts file.
Try const fooConfig: Foo = {...} in your JS file, after importing the interface. As it stands, TS is right you're declaring it twice.

Resources