Exception with type definition for random-string module - node.js

I am trying to write a .d.ts for random-string.
I have this code:
declare module "random-string" {
export function randomString(opts?: Object): string;
}
I am able to import the module no problem then with:
import randomString = require('random-string');
and invoke:
console.log(randomString); // --> [Function: randomString]
However, this doesn't work with or without an argument:
console.log(randomString({length: 10});
console.log(randomString());
I get this error from tsc:
error TS2088: Cannot invoke an expression whose type lacks a call signature.
I looked in the source for random-string and found this code for the method I am trying to interface with:
module.exports = function randomString(opts) {
// Implementation...
};
I managed to write a .d.ts for the CSON module, no problem, but that was exporting a 'class' rather than a function directly. Is that significant?

Your declaration says there is a module named random-string with a function named randomString within it...
So your usage should be:
console.log(randomString.randomString({ length: 10 }));
console.log(randomString.randomString());
If the module does actually supply the function directly, you should adjust your definition to do the same:
declare module "random-string" {
function randomString(opts?: Object): string;
export = randomString;
}
This would allow you to call it as you do in your question.

Related

Wrap a problematic npm package, while maintaining type information

The Problem
We're authoring an npm package containing React components that will be used in various (internal) web sites. There is a problematic npm package dependency that we are forced to use in our react .tsx files, that has these problems:
It doesn't expose any useful types despite having .d.ts files in it... they're empty.
It tries to run when required or imported server-side, instead of waiting until called, so we have to avoid a top-level import and instead do if (window) { const module = require('package-name') } and then use it inside that block only.
It is a frequent source of errors so everything in that library needs to be run inside of a try ... catch block.
Well, At Least We Have Types
We have already created our own types file which addressed problem #1:
// problematic-package-types.d.ts
declare module 'problematic-package' {
function doErrorProneButNecessaryThing(
foo: Record<string, unknown>,
bar: string
): void
}
The Needed Solution
The long term solution is to fix this problematic library and we're looking into how to get that done (but it's not in our direct control).
In the short term, though, we need a solution now.
Note that we are configuring dynamic requires in our npm package bundler to import them only at use-time, not treating them like other imports/requires. As our package is consumed inside other applications, we don't have full control over how that application bundling works or when the components are required, so our components may end up being required server-side when they shouldn't, and we have to tolerate that. We're still learning about some aspects of this.
My Wild (But Failed) Stab
My goal is to do something more DRY like this, where we solve all three problems of strong typing, detecting server-side execution & doing nothing, and adding error handling:
// hoping to leverage our module declaration above without importing anything
import type * as ProblematicPackage from 'problematic-package'
import wrapProblematicRequire from '../utils/my-sanity-preserving-module'
const wrappedProblematicPackage = wrapProblematicRequire<ProblematicPackage>()
// then later...
const foo: Record<string, unknown> = { property1: 'yes', property2: false }
const bar = 'yodeling'
wrappedProblematicPackage.invokeIfWindowReady(
'doErrorProneButNecessaryThing',
foo,
bar
)
However, TypeScript doesn't like the import type which unfortunately makes sense:
Cannot use namespace 'ProblematicPackage' as a type.
The Plea
How do I get the type information we've placed into problematic-package-types.d.ts to use as desired?
Or ANYTHING else. Honestly, I'm open to whatever, no matter how crude or hacky, so long as we get some clarity and reliability at call sites, with full type information as described. Suggestions/advice?
Full Details
Here is the full implementation of the wrapProblematicRequire function. I haven't tested it. It's probably awful. I'm sure it could be far better but I don't have time to get this helper module super clean right now. (My attempt to handle function type information isn't quite right.)
type Func = (...args: any[]) => any
type FunctionNames<T, TName extends keyof T> = T[TName] extends Func ? TName : never
type FunctionNamesOf<T> = FunctionNames<T, keyof T>
const wrapProblematicRequire = <T>(packageName: string) => ({
invokeIfWindowReady<TName extends FunctionNamesOf<T>>(
name: T[TName] extends Func ? TName : never,
...args: T[TName] extends Func ? Parameters<T[TName]> : never
): T[TName] extends Func ? ReturnType<T[TName]> : never {
if (!window) {
// #ts-ignore
return undefined
}
try {
// #ts-ignore
return require(packageName)[name] as T[TName](...args)
} catch (error: unknown) {
// ToDo: Log errors
// #ts-ignore
return undefined
}
}
})
export default wrapProblematicRequire
P.S. await import('problematic-package') didn't seem to work. Yes, problems abound.
Cannot use namespace 'ProblematicPackage' as a type.
Well, you can get the typeof that namespace, which seems to be what you want.
To test this, I setup the following:
// problem.js
export function doErrorProneButNecessaryThing(n) {
return n;
}
export function doErrorProneButNecessaryThing2(s) {
return s;
}
console.log('did side effect');
// problem.d.ts
export function doErrorProneButNecessaryThing(n: number): number;
export function doErrorProneButNecessaryThing2(s: string): string;
And now you can do:
import type * as ProblemNs from './problem';
type Problem = typeof ProblemNs;
// works
type A = Problem['doErrorProneButNecessaryThing'] // type A = (n: number) => number
Then the wrapProblematicRequire function just takes the name of the function as a generic, pulls the args for it, and pulls the return type.
const wrapProblematicRequire = <TName extends FunctionNamesOf<Problem>>(
name: TName,
...args: Parameters<Problem[TName]>
): ReturnType<Problem[TName]> | undefined => {
if (!window) return;
const problem = require('./problem'); // type is any, but types are enforced above
try {
return problem[name](...args);
} catch (err) {
console.log('error!');
}
};
Here require('./problem') returns the any type, but the generics keep everything key safe as long as typeof ProblemNs can be trusted.
Now to test that:
console.log('start');
const result: number = wrapProblematicRequire(
'doErrorProneButNecessaryThing',
123
);
console.log('end');
Which logs:
start
did side effect
end
Which seems to work!
Codesandbox

Typescript - Dynamic class Type

I'm trying to build a sort of model Factory in Typescript.
I'm receiving a string parameter from an API call and I would like to istantiate a new object depending on the received value.
Here you can find a simple example of what I would like to accomplish:
/classes/ClassA.ts
export class ClassA {
doSomething() {
console.log("ClassA");
}
}
/classes/ClassB.ts
export class ClassB {
doSomething() {
console.log("ClassB");
}
}
/classes/index.ts
import { ClassA } from './ClassA';
import { ClassB } from './ClassB';
export { ClassA, ClassB }
Now, I would like to import all classes exported from index.ts (this file will be automatically updated when a new Class is being created) and run doSomething() on a class depending on a variable value:
/index.ts
import * as Classes from './classes';
const className: string = "ClassA";
new Classes[className]().doSomething()
In visualStudioCode I don't get any error, but at compile time I get:
error TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'typeof import("/testApp/src/tmp/classes/index")'.
Even changing className to "any" gives the same result.
If I remove className type
const className = "ClassA";
it works without any issue but I cannot proceed in this direction because received value is "typed" as string.
I know that prepending istantiation code with
// #ts-ignore
It works but I would like to avoid this kind of "tricks"
So, what would it be the correct way to type className getting it's possible values from the imported ts?
Thanks
Micko

Extending class from external 3rd party typescript module

Hello I have a problem with overwriting types
I want overwrite a type from a libary that adds a property to an other library's typings, the line is: https://github.com/discord-akairo/discord-akairo/blob/e092ce4e0c9e749418601476bcd054a30a262785/src/index.d.ts#L14
and in my code I declare it like this:
declare module 'discord.js' {
export interface Message {
util?: KopekUtil;
}
}
KopekUtil is extending the CommandUtil and the error i get is:
TS2717: Subsequent property declarations must have the same type. Property 'util' must be of type 'CommandUtil', but here has type 'KopekUtil'. index.d.ts(16, 13): 'util' was also declared here.
You mentioned trying to extend Command util class like so
export class KopekUtil extends CommandUtil{
constructor(handler, message: Message | CommandInteraction) {
super(handler, <Message>message);
}
send(options:string | MessageOptions , reply? : boolean){
//your logic
}
}
But I'm afraid it's not possible to overwrite a class that comes from external typescript module.
Although you can introduce a new method in class from external module or extend one of existing methods using object protoype.
The right way to do that
util.js
declare module 'discord-akairo'{
export interface CommandUtil {
mySendMethod(options:string | MessageOptions , reply?:any) : boolean
}
};
CommandUtil.prototype.mySendMethod = function (options:string | MessageOptions , reply?:any) : boolean{
return true;
}
Typescript now merges interfaces and you can use your extension
const message = new Message(new Client(),{},new TextChannel(new Guild(new Client(),{})))
message.util.mySendMethod("hello")

declare global variable in seperate file nodejs+typescript

I am new to typescript and this might be a noob question.
I want to extend global variable provided by nodejs.
As per this blog I wrote this code and it is working
declare global {
namespace NodeJS {
interface Global {
appRoot: string;
}
}
}
import path from "path";
global.appRoot = path.join(__dirname,'../');
console.log(global.appRoot)
but I want to take this global to separate file and if I move this to a new global.d.ts file
I dont know what to export
I am getting this error
Augmentations for the global scope can only be directly nested in
external modules or ambient module declarations.
if do this
declare module NodeJS {
export interface Global {
appRoot: string;
}
}
I get this error
Property 'appRoot' does not exist on type 'Global & typeof globalThis'.
Property 'appRoot' does not exist on type 'Global & typeof globalThis'.
Which version of global declaration works always seems to depend on project setup. In your case, the following global.d.ts should work:
export {}; // make the file a module, to get rid of the warning
declare global {
namespace NodeJS {
interface Global {
appRoot: string;
}
}
}
Also make sure that only either one of the definitions is present.

TypeScript and Node and importing

I'm trying to use TypeScript inside Node.js with typescript-require. So I init it like:
// index.js, line 1.
require("typescript-require")({ "nodeLib": true });
And so I load the Main.ts file. Like it:
// index.js, line 2.
require("Main.ts").init();
So I have a Dictionary interface:
// Main.ts, line 8.
interface Dictionary<TValue> {
[index: string]: TValue;
}
When I put this code directly on Main.ts it run fine, like it:
// Main.ts, line 12.
var list: Dictionary<number> = {};
But I like to separate the Dictionary from Main. To allow others files use this interface without duplicate it. And here starts my problem. I don't know how I can do that. I tried a lot things, like use reference and import require, and I receive errors from all methods.
/// Without any import, I get an obvious error; or,
/// <reference path="Dictionary" />; or,
/// <reference path="Dictionary.ts" />; or,
/// <reference path="./Dictionary.ts" />; or,
import Dictionary = require("Dictionary");
>> Main.ts(6,18): error TS2304: Cannot find name 'Dictionary'.
import Dictionary = require("Dictionary.ts");
>> Main.ts(1,29): error TS2304: Cannot find name 'Dictionary'.
>> Main.ts(6,18): error TS2315: Type 'any' is not generic.
In all cases, seems that Main.ts doesn't include the file correctly, so I can't reuse that. So I'm forgetting something?
Before I post it I found the solution. I was trying export interface on file Dictionary.ts like it:
export interface ... { ... }
But I need export = it. Like:
interface Name { ... }
export = Name;

Resources