Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create Module containing all exports in TypeScript

What am I trying to achieve

I am trying to create a module in TypeScript which would contain every export of needed classes. In fact, what bothers me actually is to have import statements in my classes which are all relative, which is not really the best way for further maintenances. I would like to use absolute paths only.

Once transpiled, classes (in the produced JavaScript) with absolute paths have wrong paths (since they are not translated to relative paths, and even worse when used in conjunction with tsconfig.json > outDir option).

Example usage

Let's start with the following hierarchy of project as example

root
|- helpers
   |- MyHelper.ts
   |- AnotherHelper.ts
|- calculators
   |- MyCalc.ts
|- parsers
   |- XmlParser.ts
   |- JsonParser.ts
|- index.ts // currently holding export statements

What I want to do

I would like to have an index.ts file of this kind :

// a top-level module, resolved like the `node` strategy of NodeJS module resolution
export module IndexModule {
    export { MyHelper } from "./helpers/MyHelper";
    export { AnotherHelper } from "./helpers/AnotherHelper";
    // do the same for every class in my project
}

So then I could do the following :

import { JsonParser } from "IndexModule"; // no relative nor absolute path

export class MyHelper {
    public constructor(input: any){
       var result: any = new JsonParser().parse(input);
       // work with result ...
    }
}

I've seen in many places (here on SO, the github repo of TypeScript, ...) that I am not the only one struggling with relative/absolute paths for imports.

My actual question is : Is it possible to create such a module (or mechanic) so I can define every export in a top container, and import this container as if I was using a NodeJS module (like import * as moment from "moment") ?

What I tried

First try

This is the index.ts file I actually have :

// index.ts is in the root directory of the project, it accesses classes with relative paths (but here, management is quite easy)
export { Constants } from "./helpers/Constants";
export { ConstraintCalculatorHelper } from "./helpers/ConstraintCalculatorHelper";
export { MomentHelper } from "./helpers/MomentHelper";
export { Formatter, OutputFormatterHelper } from "./helpers/OutputFormatterHelper";
// ... doing this for each class I have in my project

Second try

declare module IndexModule {
    export { Constants } from "./helpers/Constants";
    export { ConstraintCalculatorHelper } from "./helpers/ConstraintCalculatorHelper";
    export { MomentHelper } from "./helpers/MomentHelper";
    export { Formatter, OutputFormatterHelper } from "./helpers/OutputFormatterHelper";
    // error : Export declarations are not permitted in a namespace
}

Third, Fourth, N-th try

Tried to declare namespace, export module, etc etc .. without luck.

Tried to declare module "IndexModule too, leads to an error : Import or export declaration in an ambient module declaration cannot reference module through relative module name

Side note

I am not an expert in NodeJS / Modules / Namespaces, so I maybe have not understood something well. If so, I would appreciate if someone can point out what I misunderstood.

Relevant link here that shows the actual problem.

Configuration :

  • NodeJS v7.2
  • TypeScript v2.2

tsconfig.json :

{
  "compileOnSave": true,
  "compilerOptions": {
    "removeComments": false,
    "sourceMap": true,
    "module": "commonjs",
    "target": "es6",
    "noImplicitAny": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./bin/LongiCalc"
  }
}
like image 442
Jacks Avatar asked Nov 07 '22 22:11

Jacks


1 Answers

I finally managed to get something like I wanted. Here are the steps and an explanation about it.

1. Architecture example

To illustrate the problem/solution, let's take this arborescence as an example

root
  \- A
     \- a1.ts
     \- a2.ts
  \- B
     \- D
        \- bd1.ts
        \- bd2.ts
     \- b1.ts
  \- C
     \- c1.ts
     \- c2.ts

There is a root folder, containing 3 other folders (A, B and C) :

  • A contains two files (a1.ts and a2.ts)
  • B contains a folder with two files (D, with bd1.ts and bd2.ts) and a file (b1.ts)
  • C contains two files (c1.ts and c2.ts)

So now, if bd1.ts needs classes declared in a1.ts, we will currently do ..

import { ClassFromA1 } from "../../A/a1"

.. which uses relative paths and is hard to maintain (IMO). We will get rid of that in the next steps.

2. Create an index.ts with all imports

The purpose of the index.ts file (I named it like this, you can choose whatever name you want) is to keep all needed classes in one single file.

So what we will do, is to create this file on the top of our arborescence in a node_modules directory, and export every single class we need in our project. This way, we will only need the index.ts file to retrieve the class we want.

Here are the contents :

// current position : root/node_modules/index.ts
export { ClassFromA1 }  from "./A/a1.ts"
export { ClassFromA2 }  from "./A/a2.ts"
export { ClassFromBD1 } from "./B/D/bd1.ts"
export { ClassFromBD2 } from "./B/D/bd2.ts"
export { ClassFromB1 }  from "./B/b1.ts"
export { ClassFromC1 }  from "./C/c1.ts"
export { ClassFromC2 }  from "./C/c2.ts"

Now that we have got this, the previous import can be done this way ..

import { ClassFromA1 } from "../../index"

.. but this still implies us to use relative paths, not optimal.

Setting in the project a rootDir to the root folder can solve this problem ..

import { ClassFromA1 } from "index"

.. and it works ! But the problem happens with transpiled classes, where paths are not resolved as relative once compiled.

This means your a1.js (compiled a1.ts file) will still have the import set as it is, but will probably be wrong because it has no knowledge of rootDir.

// a1.js
const index_1 = require("index") // supposed to be in the same package than the current file

3. Select the node resolution strategy

Fortunately, NodeJS has a node resolution strategy that can be set to resolve node module imports. Here is a really brief summary of the Node module resolution strategy that can be set in our project :

  • Check the import path
  • If the import path begins with ., .., or / => it is relative
  • If the import path begins with a name => it is a node module (this is important)

The resolution of modules in NodeJS is as it follows (starting always from bs1.ts) :

  • Check if root/B/D/node_modules/index.ts exists > No
  • Check if root/B/node_modules/index.ts exists > No
  • Check if root/node_modules/index.ts exists > Yes !

What we have done here, is that we tricked Typescript to think that index.ts is a NodeJS module and that it must be resolved as one.

It will use the pattern described above (and in the link) to retrieve index.ts, and from it we can then import the needed classes without relative / absolute paths.

4. Do imports as we wanted to

Now imports can be done the following way ..

// no matter in which file we are
import { ClassFromA1 } from "index";
import { ClassFromBD1 } from "index";
// and so on ..

.. and it works nicely, no errors on VisualStudio, nor transpiled classes. Any remarks on this solution is welcome.

Cheers !

like image 122
Jacks Avatar answered Nov 14 '22 22:11

Jacks