Typescript module augmentation with ambient declaration - typescript-typings

Just a question on whether this problem with typings is possible to fix with an ambient type/module declaration and how. In actuality I'll see about getting the typings fixed in the library in question, but sometimes that's not possible.
I'm trying #finos/perspective. There is a worker object that has a factory method for creating a table: worker.table.
#finos/perspective/index.d.ts
export type PerspectiveWorker = {
table(data: TableData, options?: TableOptions): Table;
};
The data parameter is of type TableData.
#finos/perspective/index.d.ts
export type TableData = string | Array<object> | {[key: string]: Array<object>} | {[key: string]: string};
The problem is that worker.table() also actually takes an ArrayBuffer to allow creation of a table from an Apache Arrow binary buffer. So, TableData should have | ArrayBuffer added to it's type signature, but since it does not have this typescript is complaining:
Argument of type 'ArrayBuffer' is not assignable to parameter of type 'string | object[] | { [key: string]: object[]; } | { [key: string]: string; } | Schema'.
Type 'ArrayBuffer' is not assignable to type 'Schema'.
Index signature is missing in type 'ArrayBuffer'.ts(2345)
If I manually edit #finos/perspective/index.d.ts and add | ArrayBuffer as in:
export type TableData = string | Array<object> | {[key: string]: Array<object>} | {[key: string]: string} | ArrayBuffer;
Then typescript stops complaining (and the code actually works regardless of whether typescript complains or not).
Is there a way to fix this with an ambient declaration?
tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"skipLibCheck": true,
"lib": ["dom", "esnext", "ESNext.AsyncIterable"],
"declaration": true,
"outDir": "lib",
"rootDir": "src",
"strict": true,
"types": ["node"],
"esModuleInterop": true,
"resolveJsonModule": true,
"downlevelIteration": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"typeRoots": ["./node_modules/#types", "./src/_declarations"]
},
"exclude": ["node_modules"]
}
src/_declarations/perspective.d.ts
import type { TableData } from "#finos/perspective";
declare module "#finos/perspective" {
export type TableData =
| string
| Array<Record<string, unknown>>
| { [key: string]: Array<Record<string, unknown>> }
| { [key: string]: string }
| ArrayBuffer;
}
I also tried:
export type TableData = TableData | ArrayBuffer;
All of this is failing to stop typescript from complaining. I sometimes struggle with things like this, trying to augment a third-party module via ambient type declarations and wonder if anyone could shed some light on how this might be solvable without changing the third-party library.

Related

Typescript and Subpath Imports

I am trying to get Node subpath imports and typescript to work. My IDE has no problem resolving the imports, but Typescript is never happy.
Github repo with code: https://github.com/doronrosenberg/ts-subpath-imports.
package.json:
"imports": {
"#internal/*": "./internal/*.ts",
"#internal2": "./internal"
}
tsconfig.json:
"paths": {
"#internal/*": "./internal/*.ts",
"#internal2": ["./internal"]
}
and the code:
import { foo } from "#internal/index";
import { bar } from "#internal2";
No matter how I set things up, I always get:
src/test.ts:1:21 - error TS2307: Cannot find module '#internal/index' or its corresponding type declarations.
1 import { foo } from "#internal/index";
~~~~~~~~~~~~~~~~~
src/test.ts:2:21 - error TS2307: Cannot find module '#internal2' or its corresponding type declarations.
2 import { bar } from "#internal2";
~~~~~~~~~~~~
Any ideas?
After hacking around with Typescript for a few weeks, I got a working solution.
Let's say I have a package called #kodadot1/metasquid with multiple submodules (consolidator and entity).
In my package.json I declare imports
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./consolidator": {
"types": "./dist/consolidator.d.ts",
"import": "./dist/consolidator.mjs",
"require": "./dist/consolidator.cjs"
},
"./entity": {
"types": "./dist/entity.d.ts",
"import": "./dist/entity.mjs",
"require": "./dist/entity.cjs"
}
}
The trick is to create a .d.ts file for each submodule in the project's root.
So for submodule entity I will make a file called entity.d.ts that contains
export * from './dist/entity'
Now to publish it correctly in npmjs extend your package.json like:
"files": [
"dist",
"*.d.ts"
]
Now just publish, and you can enjoy subpath imports:
import { get } from '#kodadot1/metasquid/entity'
Whole code is available here
The support of subpath exports requires newer module resolutions such as Node16 and NodeNext:
{
"compilerOptions": {
"moduleResolution": "Node16" // or `"NodeNext"`
}
}

How to import esm modules with NodeJS 13, and Typescript 3.8?

I have a problem with some imports in NodeJS. I want to use the new features of Typescript 3.8, like private fields : #myPrivateField
I don't know how to correctly import the module "typescript" in my class. I tried many options, but impossible to solve my problem.
My files :
package.json
{
"name": "test",
"scripts": {
"start": "tsc && node --experimental-modules --es-module-specifier-resolution=node main.js"
},
"dependencies": {
"#types/node": "^13.13.2",
"app-root-path": "^3.0.0",
"fs-extra": "^9.0.0",
"tsutils": "^3.17.1"
},
"devDependencies": {
"ts-node": "~8.3.0",
"typescript": "^3.8.3"
},
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"lib": [
"ESNext",
"es2016",
"dom",
"es5"
],
"module": "esnext",
"moduleResolution": "Node",
"sourceMap": true,
"target": "es6",
"typeRoots": [
"node_modules/#types"
]
}
}
main.ts
// import ts = require("typescript");
import * as ts from "typescript";
export class Main {
node: ts.Node;
#test = 'zzz';
constructor() {}
process(): void {
ts.forEachChild(this.node, function cb() {
});
console.log('#test', this.#test);
}
}
const main = new Main();
main.process();
With this code, when I run npm run start, I have the error TypeError: ts.forEachChild is not a function
Without the line with ts.forEachClid() it logs correctly the value of the private field #test.
If I try to replace import * as ts from "typescript"; by import ts = require("typescript");, I have the error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead
Of course, I tried many changes in tsconfig.json and in package.json (with `"type" = "module"), but impossible to solve this problem.
I even tried to replace "module": "esnext" by "module": "commonjs", but I have an error exports is not defined.
Remark :
This is not specific to the module "typescript". I have the same problem with other modules like "fs-extra", which are making exports in a different way than most of classic NodeJS modules.
For example, the module "typescript" exports with export = ts.
I found this reference too, but it didn't help me a lot :
https://www.typescriptlang.org/docs/handbook/modules.html
My nodeJs version is 13.3.0 and my typescript version is 3.8.3
Thanks for your help
Finally, the good response was : you need commonjs and es2018 to be able to use the typescript #privateFields in a node module.
Here is the correct tsconfig.json to use :
{
"compileOnSave": false,
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es2018",
"typeRoots": [
"node_modules/#types"
],
"lib": [
"es2018",
"dom"
]
}
}
ESM Modules are still in experimental mode, there are a lot of modules that do not support it, e.g. uses module.exports (CommonJS) instead of export (ESM).
Your best choice here is to use commonjs module and run node without any flags. Also, type of the package in package.json must be commonjs as well.
Answering your question "How to import ESM module"... In case, module does support ESM modules, you can just use import as you usually do:
import { something } from './something';
UPD: as OP author mentioned, for private fields to work, there is must be set a target es2018. The reasoning behind this is that private fields is not a part from ES2015 specification and you need to upgrade to the minimal supported target to do that.
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es2018"
}

Creating a custom visual for PowerBI in NodeJS - "Cannot find name 'IVisualHost'

I'm trying to follow this tutorial on creating a custom visual for Power BI : https://learn.microsoft.com/en-us/power-bi/developer/custom-visual-develop-tutorial
The test with the default code works properly when I connect to Power BI Cloud, as shown in the part"Testing the custom visual" step 8, of the tutorial.
The problem is when I try to add the class-level properties in the visual.ts file (after I deleted the code as indicated in the part "Developing the visual elements" step 2 of the tutorial ), I get the error "Cannot find name 'IVisualHost'".
"use strict";
import "core-js/stable";
import "./../style/visual.less";
import powerbi from "powerbi-visuals-api";
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import IVisual = powerbi.extensibility.visual.IVisual;
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions;
import VisualObjectInstance = powerbi.VisualObjectInstance;
import DataView = powerbi.DataView;
import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject;
import { VisualSettings } from "./settings";
export class Visual implements IVisual {
private host: IVisualHost; ------------ the first error is here
private svg: d3.Selection<SVGElement>;
private container: d3.Selection<SVGElement>;
private circle: d3.Selection<SVGElement>;
private textValue: d3.Selection<SVGElement>;
private textLabel: d3.Selection<SVGElement>;
constructor(options: VisualConstructorOptions) {
this.svg = d3.select(options.element) ----------- the second error is here
.append('svg')
.classed('circleCard', true);
this.container = this.svg.append("g")
.classed('container', true);
this.circle = this.container.append("circle")
.classed('circle', true);
this.textValue = this.container.append("text")
.classed("textValue", true);
this.textLabel = this.container.append("text")
.classed("textLabel", true);
}
I also have this other error in the same visual.ts file : 'd3' refers to a UMD global, but the current file is a module. Consider adding an import instead. I imported the library with the commands : "npm i d3#3.5.5 --save" and "npm i #types/d3#3.5"
This is my pbiviz.json file :
{
"visual": {
"name": "visual9basic",
"displayName": "visual9basic",
"guid": "visual9basic252E75AF09794C8F8CE14414674FBC3E",
"visualClassName": "Visual",
"version": "1.0.0",
"description": "",
"supportUrl": "",
"gitHubUrl": ""
},
"apiVersion": "2.6.0",
"author": {
"name": "",
"email": ""
},
"assets": {
"icon": "assets/icon.png"
},
"externalJS": [
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js",
"node_modules/d3/d3.min.js"
],
"style": "style/visual.less",
"capabilities": "capabilities.json",
"dependencies": null,
"stringResources": []
}
Here is my tsconfig.json :
{
"compilerOptions": {
"allowJs": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"outDir": "./.tmp/build/",
"moduleResolution": "node",
"declaration": true,
"lib": [
"es2015",
"dom"
]
},
"files": [
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js",
"./src/visual.ts",
"./src/settings.ts",
]
}
In Visual.ts, try adding the following import:
import IVisualHost = powerbi.extensibility.IVisualHost;
It should take care of the missing interface reference.
For the d3 reference you can try
import * as d3 from "d3";
Hope this helps. I haven't worked through the tutorial myself yet.
What I found out is that after a certain patch of npm, the ".api" folder isn't created any more. Instead, those files are stored under "node_modules/powerbi-visuals-api/". Also, the file "index.d.ts" replaces the former "PowerBI-visuals.d.ts".
But, I still have the 2 errors I described.

Typescript dynamic import() of file only works with hardcoded strings

I am trying to dynamically load a file in typescript using import(). I have managed to get it working if I hardcode the path to the file, as this:
import("./file").then(module => doStuff(module)); // Success
But as soon as I store the path in a variable, it will not find the file.
This will succeed to compile but will at runtime complain: Cannot find module "./file".
path = "./file";
import(path).then(module => doStuff(module)); // Cannot find module "./file".
Is there any way to fix this, or any way to work around for the issue?
tsconfig.json:
{
"compilerOptions": {
"sourceMap": true,
"target": "es6",
"module": "esnext",
"strict": true,
"allowJs": true,
"lib": [
"esnext"
],
"moduleResolution": "node"
},
"exclude": [
"node_modules"
]
}
Yup, this is an interesting one lol
prefix your import with '../../' or similar (play around with it)
e.g. import(../../${m}.ts)
Only way to do it as the issue is with the import itself and not typescript or others.

How have IDE type awareness with app-module-path in Visual Studio Code

I'm in a node environment where they are using app-module-path so that the requires can always be written from a base path.
So all path's can be: const something = require('/api/something') instead of having to back down the folder structure with something like: const something = require('../../api/something).
Without the exact path, VSCode says all types are of type any. This means functionality like 'go to definition' won't work.
Is there a way to configure VSCode to address this?
After some more searching I found this can be done by using a jsconfig.son file with the following settings.
{
"compilerOptions": {
"target": "ES6",
"allowSyntheticDefaultImports": true,
"jsx": "react",
"module": "commonjs",
"baseUrl": ".",
"paths": {
"*": [
"*",
"app/*"
]
}
},
"include": [
"app/**/*.js"
]
}

Resources