I have a sample typescript objects as
declare const S3 = "https://s3.amazonaws.com/xxx/icons";
declare const SVG = "svg-file-icons";
declare interface MyIcons {
"image/jpeg": string;
"image/jpg": string;
}
export const FILE_ICONS_SVG: MyIcons = {
"image/jpeg": `${S3}/${SVG}/jpg.svg`,
"image/jpg": `${S3}/${SVG}/jpg.svg`
};
I am declaring this object in a share NPM Package to maintain consistency in all my projects. But TSC compilation gives me something like this.
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FILE_ICONS_SVG = {
"image/jpeg": `${S3}/${SVG}/jpg.svg`,
"image/jpg": `${S3}/${SVG}/jpg.svg`
};
As it is evident that S3 and SVG are not defined in the compiled js file and thus gives errors on usage.
How can this be fixed??
Using declare does not really "declare" something.
declare is only used to tell the type-system that something with the declared name and type exists.
If you want to define a constant that should exist outside of the type-system, aka exist at runtime, you have to remove the declare keyword.
declare'd things do not have any impact on the runtime
Why does declare exist?
If you think about how the web works, you have a html file. In that html you can include scripts. Those scripts may be completely independent from one another, but also use stuff from other scripts.
So if you have one file that attaches something to the window for example in one file, and have another file that then uses this object, the typescript type-system has no way of knowing that that object exists, so you can tell the type-system of its existence by using declare
So it should be
const S3 = "https://s3.amazonaws.com/xxx/icons";
const SVG = "svg-file-icons";
Related
I am using a javascript library that constructs an object like so:
usefulJS.js
function usefulJS() {}
usefulJS.protoype.doAThing = function() {
//does a thing here, returns an object
};
module.exports = new usefulJS();
It has a type definition file like so:
usefulJS/index.d.ts
export class usefulJS {
public doAThing(): object;
}
And it is used in the following typescript file:
myTypescript.ts
import {randomOtherThing,usefulJS} from "library-that-includes-usefulJS";
const myObj = usefulJS.doAThing();
But I get a red underline under .doAThing() in my typescript file with the following message:
"Property 'doAThing' does not exist on type 'typeof usefulJS'."
When I run the code with //#ts-ignore over the function call it works, so it definitely seem to be a typescript issue and not a javascript issue.
Am I doing something wrong with my imports? How should I import an object that is already constructed, as opposed to a prototype for an object?
Or is there something wrong with the library's type definitions?
If it is any help, there is another project in non-typescript node that uses this library. Its import line looks like this:
const usefulJS = requre("library-that-includes-usefulJS").usefulJS;
Just a guess: TypeScript probably locate usefulJS from usefulJS.js instead of index.d.ts (you can check if that's the case by using "go to definition" in VS Code). If that's the case TypeScript will not recognize that usefulJS has a doAThing method, because it is assigned through prototype. You can probably solve this by adding this to your tsconfig.json:
"compilerOptions": {
"baseUrl": "./",
"paths": {
"library-that-includes-usefulJS": [
"./usefulJS/index.d.ts"
]
}
}
By making library changes I am able to get it to work.
Basically, the index.d.ts file doesn't indicate that helpfulJS.js exports a value. Unfortunately, we can't indicate that it exports the value helpfulJS, because that would result in a name overlap with the type. So helpfulJS.js needs to be modified to export a different variable name.
usefulJS.js
function usefulJS() {}
usefulJS.protoype.doAThing = function() {
//does a thing here, returns an object
};
let usefulJSObj = new usefulJS();
module.exports = usefulJSObj;
usefulJS/index.d.ts
export class usefulJS {
public doAThing(): object;
}
export const usefulJSObj : usefulJS;
myTypescript.ts
import {randomOtherThing,usefulJSObj} from "library-that-includes-usefulJS";
const myObj = usefulJSObj.doAThing();
I do have the ability to change this library in this case but I am loath to do so because it has impact on other projects.
I am still hoping there is a way to fix this without changing the library (or maybe only changing the type file, since the other users of the library are javascript).
I found a way to change the library that solves the problem without making any of the existing javascript clients change their code.
Basically, by changing the library to export a type with static functions instead of an unnamed object with object functions, typescript stops complaining about being unable to call the method and the javascript calls to the library don't change at all.
(usefulJS.doAThing() with usefulJS as an object calling its prototype function changes to usefulJS.doAThing() as a type calling its static function, with javascript not caring about the difference.)
usefulJS.js
class usefulJS() {
static doAThing() {
//does a thing here, returns an object
};
}
//Returns a type with a static method instead of a constructed object
module.exports = usefulJS;
usefulJS/index.d.ts
export class usefulJS {
public static doAThing(): object;
}
myTypescript.ts
import {randomOtherThing,usefulJS} from "library-that-includes-usefulJS";
const myObj = usefulJS.doAThing();
Still doesn't help much for those who don't have control over the library, unfortunately, but at least we don't cause exploding changes to all the javascript clients that already worked just to make typescript clients work.
I'm pretty new to TypeScript and trying to make automated tests where I dependency-inject IndexedDB into my code, but the library I'm using to mock IDB doesn't have a .d.ts, so I tried to roll my own simple declaration file to get around this, which looked something like this:
declare let fakeIndexedDB: IDBFactory;
declare module 'fake-indexeddb' {
export var fakeIndexedDB;
}
However, when I tried using this type, I got the error:
type 'typeof import("fake-indexeddb")' is missing the following properties from type 'IDBFactory': cmp, deleteDatabase, open
Mousing over in VSCode, it looks like the type of IDBFactory is this type from lib.dom.d.ts:
declare var IDBFactory: {
prototype: IDBFactory;
new(): IDBFactory;
};
But what I wanted to import was the interface type directly above it. How would I say in my declaration file that I want to reference the interface in lib.dom.ts, not the var that uses it? I can see that jsdom was able to make a class in their .d.ts that references DOM types with both an interface and var, but they also don't use "declare module".
Problem
The problem with this code:
declare let fakeIndexedDB: IDBFactory;
declare module 'fake-indexeddb' {
export var fakeIndexedDB;
}
is that the type of the exported fakeIndexedDB is any. It's a different variable than the one declared above it. Basically, fake-indexeddb defined like that is a module that exports a single variable called fakeIndexedDB of unspecified type.
Solution
What you should do instead is this:
declare module 'fake-indexeddb' {
const fakeIndexedDB: IDBFactory;
export = fakeIndexedDB;
}
Whether to use export =, export or export default depends on how the actual JavaScript library is built. The above syntax is recommended if fake-indexeddb exports a single member and it's meant to work well when imported using the require function. See if it works, and if not, consult the source code.
I need to have some strongly-typed global variables.
As mentioned here: Extending TypeScript Global object in node.js, in order to add fields to the global variable I need to add a .d.ts file that extends the Global interface that's specified in node.d.ts.
Also, as Basarat mentioned:
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.
Now, I need to have fields on the Global interface whose types are custom interfaces that I created:
declare namespace NodeJS{
interface Global {
foo: Foo
bar: Bar
}
}
I'm extremely not willing to use the any type.
I can move/copy all the interface declarations to this declaration file, but it's a bad solution for me, since both Foo and Bar in turn, aggregate many fields of other interfaces, including third party interfaces like Moment etc.
I need a solution for this paradox
Here's an approach. I don't know if this is the 'correct' way of doing things, but it works for me with TypeScript 3.7.4.
Assuming your source files live in a folder src, create a new folder src/types and create a file global.d.ts in this folder.
Author your declarations using one of the following strategies:
If you need to import external types into your declaration file, use the following syntax:
import { Express } from 'express';
declare global {
namespace NodeJS {
interface Global {
__EXPRESS_APP__: Express;
}
}
}
If your declaration file does not contain any imports, the above will not work, and you'll need to use this syntax instead:
declare namespace NodeJS {
interface Global {
__CONNECTION_COUNT__: number;
}
}
Make sure your global.d.ts file (and any other files you might add to src/types) is picked up by the TypeScript compiler, by adding the following to your tsconfig.json file:
{
"paths": {
"*": ["node_modules/*", "src/types/*"]
}
}
Use the global variable as normal inside your code.
// Below, `app` will have the correct typings
const app = global.__EXPRESS_APP__;
I found this works.
Have one file that declares the property on the NodeJS.Global interface with the any type. This file has to be clean of imports or refrences.
node.d.ts
declare namespace NodeJS{
interface Global {
foo: any
}
}
Then in the second file you declare a global variable that has the correct type.
global.d.ts
import IFoo from '../foo'
declare global {
const foo:Ifoo
}
This worked for me (node v16.13.2)
In your root create file types/global.d.ts
declare global {
var __root: string
}
export {}
Note that __root declared with var keyword. It works with let and const too, but in this case __root will have any type. I don't know why;) If someone can explain this it will be great.
Configure your tsconfig.json
{
"compilerOptions": {
"typeRoots": [
"types"
],
}
}
Use declared variable in your code
// app.ts (entry point)
import path from 'path'
global.__root = path.join(__dirname)
// anyFileInProject.ts
console.log(__root) // will display root directory
I have an external JS library with a global parameter:
function Thing() { ... }
...
var thing = new Thing();
There is a TypeScript definition file, so in thing.d.ts:
declare var thing: ThingStatic;
export default thing;
export interface ThingStatic {
functionOnThing(): ThingFoo;
}
export interface ThingFoo {
... and so on
Then I import this into my own TS files with:
import thing from 'thing';
import {ThingFoo} from 'thing';
...
const x:ThingFoo = thing.functionOnThing();
The problem is that transpiles to:
const thing_1 = require("thing");
...
thing_1.default.functionOnThing();
Which throws an error. I've asked about that in another question, and the suggestion is to use:
import * as thing from 'thing';
That doesn't fix it - it gives me thing.default in TS but then that's undefined once transpiled to JS.
I think there's something wrong with thing.d.ts - there must be a way to define a typed global parameter that can be imported.
How should I write thing.d.ts so that it represents the JS correctly and doesn't transpile to include default or other properties not actually present?
If the only way to use that library is by accessing its globals (as opposed to importing it as node module or amd or umd module), then the easiest way to go is have a declaration file without any exports at top level. Just declaring a variable is enough. To use it, you have to include that declaration file when compiling your typescript code, either by adding it to files or include in tsconfig.json, or directly on command line. You also have to include the library with a <script> tag at runtime.
Example: thing.d.ts
declare var thing: ThingStatic;
declare interface ThingStatic {
functionOnThing(): ThingFoo;
}
declare interface ThingFoo {
}
test-thing.ts
const x:ThingFoo = thing.functionOnThing();
can be compiled together
./node_modules/.bin/tsc test-thing.ts thing.d.ts
the result in test-thing.js:
var x = thing.functionOnThing();
See also this question about ambient declarations.
Note: there are module loaders out there that allow using global libraries as if they were modules, so it's possible to use import statement instead of <script> tag, but how to configure these module loaders to do that is another, more complicated question.
Now I am sure the issue is because there is a d.ts file included which contains a module called "Shared", and a require statement which includes a variable of the same name if it is being used in a NodeJS environment.
// shared.d.ts
declare module Shared { ... }
// other_module.ts
/// <reference path="shared.d.ts"/>
if(require) { var Shared = require("shared"); }
export class Something {
public someVar = new Shared.SomethingElse("blah");
}
So when I compile other_module.ts (which is actually a lot of separate files), it tells me Shared is a duplicate identifier, which I can understand as TS thinks Shared is a module, but then is being told it is the return of require.
The problem here is that the output of modules need to be compatible with nodeJS's require system, so in this case when other_module is required it will be in its own scope and will not know about Shared.SomethingElse so the require is needed so the internal modules in other_module will be able to access the Shared library, but in the browser environment it would get Shared.SomethingElse via the global scope.
If I remove the reference then the file wont compile as it doesn't know about Shared, if I remove the require when the module is loaded into nodejs (var otherModule = require("other_module")) it will complain that it doesn't know about Shared. So is there a way to solve this?
First the error
Duplicate identifier because you have Shared in shared.d.ts + in other_module.ts.
FIX A, be all external
If you want to use amd / commonjs ie. external modules, you need to use import/require (not var/require like you are doing). Using an import creates a new variable declaration space and therefore you are no longer polluting the global namespace Shared from other_module.ts. In short :
// shared.d.ts
declare module Shared {
export function SomethingElse(arg:string):any;
}
declare module 'shared'{
export = Shared;
}
And a typesafe import:
// other_module.ts
/// <reference path="shared.d.ts"/>
import Shared = require("shared");
export class Something {
public someVar = new Shared.SomethingElse("blah");
}
FIX B, as you were, but you need to use a different name then
Inside other_module don't use the name Shared locally if local scope is global scope. I recommend you just use external everywhere and compile for node with commonjs and browser with amd as shown in fix A, but if you must here is a compile fixed other_module.ts.
// other_module.ts
/// <reference path="shared.d.ts"/>
var fooShared: typeof Shared;
if(require) { fooShared = require("shared"); }
else { fooShared = Shared; }
export class Something {
public someVar = new fooShared.SomethingElse("blah");
}