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

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.

Related

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.

Cannot find module error when importing a module with exported enums

I'm trying to import a custom module that exports enum types. However, when I try to use the exported enums I get this error Error: Cannot find module 'Runescape'
The code works if I either do not use the enums, so the import statement does work, or if I move the enums into my main ts file. I've tried with the compiler option "preserveConstEnums"
tsconfig.json
{
"compilerOptions": {
"resolveJsonModule": true,
"moduleResolution": "node",
"esModuleInterop": true,
"sourceMap": true,
"outDir": "out",
"target": "es6"
}
}
Runescape.ts
declare module 'Runescape' {
...
/**
* #description Enum of all Runescape skills
* #enum
* #default
*/
export enum SkillsEnum {
ATTACK = 'attack',
STRENGTH = 'strength',
DEFENSE = 'defense',
RANGED = 'ranged',
PRAYER = 'prayer',
MAGIC = 'magic',
RUNECRAFT = 'runecraft',
CONSTRUCTION = 'construction',
HITPOINTS = 'hitpoints',
AGILITY = 'agility',
HERBLORE = 'herblore',
THIEVING = 'thieving',
CRAFTING = 'crafting',
FLETCHING = 'fletching',
SLAYER = 'slayer',
HUNTER = 'hunter',
MINING = 'mining',
SMITHING = 'smithing',
FISHING = 'fishing',
COOKING = 'cooking',
FIREMAKING = 'firemaking',
WOODCUTTING = 'woodcutting',
FARMING = 'farming'
}
...
I expect to be able to run code like this
const OSRS_SKILLS_VALUES: string[] = Object.keys(Runescape.SkillsEnum).map(
(key: string): string => Runescape.SkillsEnum[key]
)
and not have the module not found error thrown
This is because Runescape.ts (at least the part you specified in your question) is not a module, it's a file that declares a module (and probably should have been named Runescape.d.ts).
Is Runescape.ts your code or a 3rd party?
If it is your code and you have additional code that makes the "Runescape" module, you should put and export that enum in that module, typescript will take care of generating the deceleration file for you. If you just want to create an enum, export it directly from "Runescape.ts" (without the module declaration).
If it's not your code and you can't modify it, you can still use it by telling typescript compiler where to find the declaration of the module, you do it by adding a reference directive (adjust the path to the actual file, if needed):
/// <reference path="Runescape.ts" />
Then you can use it by importing the module:
import * as Runescape from "Runescape";
const OSRS_SKILLS_VALUES: string[] = Object.keys(Runescape.SkillsEnum).map(
(key: string): string => Runescape.SkillsEnum[key]
)
Or just directly the enum:
import { SkillsEnum } from "Runescape";
const OSRS_SKILLS_VALUES: string[] = Object.keys(SkillsEnum).map(
(key: string): string => SkillsEnum[key]
)
Note that in this case, just because typescript can compile this code doesn't mean it can be executed. Because the enum is just a declaration, the actual javascript code will assume there is already a module named "Runescape" that contains the real code of that enum (actually, it's javascript equivalent, there are no enums in javascript). It is your responsibility that this module is accessible in the runtime environment (node, browser, etc).
To understand better the concepts of modules and declarations files, you should read the typescript docs on modules, namespaces and modules and other related sections in the docs.

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.

Extending TypeScript Global object in 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.

Resources