122

Let's say I have two projects with following file structure

/my-projects/

  /project-a/
    lib.ts
    app.ts
    tsconfig.json

  /project-b/
    app.ts         // import '../project-a/lib.ts'
    tsconfig.json

I want to consume lib.ts located in project-a also from project-b. How to do that?

  • Release it as NPM module - absolutely don't want that, it's an overkill for such a simple use case. I just want to share one file between two projects.

  • Use import '../project-a/lib.ts' - doesn't work, TypeScript complains

'lib.ts' is not under 'rootDir'. 'rootDir' is expected to contain all source files.

  • Put tsconfig.json one level up so it would cover both project-a and project-b - can't do that, the TypeScript config is slightly different for those projects. Also it's not very convenient, don't want to do that.

Any other ways?

Alex Craft
  • 13,598
  • 11
  • 69
  • 133
  • 1
    Two suggestions: a) you could put `project-a` on GitHub, [and then add that to your `project-b` package.json](https://docs.npmjs.com/files/package.json#github-urls), or b) you could use [`tsconfig` inheritance to share a config and then just override things for the two packages](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends). – Joe Clay Dec 09 '17 at 14:10
  • 1
    It's also possible to [use local paths as NPM dependencies](https://docs.npmjs.com/files/package.json#local-paths), if you'd rather not make your code public. – Joe Clay Dec 09 '17 at 14:12
  • so did you find a solution to sharing the code between angular and express apps? – Toolkit Jul 08 '18 at 04:45

7 Answers7

127

Since Typescript 3.0 this can be done with Project References.

Typescript docs: https://www.typescriptlang.org/docs/handbook/project-references.html

I believe you would have to move lib.ts into a small ts project called something like 'lib'

The lib project should have a tsconfig containing:

// lib/tsconfig.json
    {
          "compilerOptions": {
            /* Truncated compiler options to list only relevant options */
            "declaration": true, 
            "declarationMap": true,
            "rootDir": ".",   
            "composite": true,     
          },
          "references": []  // * Any project that is referenced must itself have a `references` array (which may be empty).
        }

Then in both project-a and project-b add the reference to this lib project into your tsconfig

// project-a/ts-config.json
// project-b/ts-config.json
{
        "compilerOptions": {
            "target": "es5", 
            "module": "es2015",
            "moduleResolution": "node"
            // ...
        },
        "references": [
            { 
                "path": "../lib",
                // add 'prepend' if you want to include the referenced project in your output file
                "prepend": true, 
            }
        ]
    }

In the lib project. Create a file index.ts which should export all your code you want to share with other projects.

// lib/index.ts    
export * from 'lib.ts';

Now, let's say lib/lib.ts looks like this:

// lib/lib.ts
export const log = (message: string) => console.log(message);

You can now import the log function from lib/lib.ts in both project-a and project-b

// project-a/app.ts 
// project-b/app.ts
import { log } from '../lib';
log("This is a message");

Before your intelissense will work, you now need to build both your project-a and project-b using:

tsc -b 

Which will first build your project references (lib in this case) and then build the current project (project-a or project-b).

The typescript compiler will not look at the actual typescript files from lib. Instead it will only use the typescript declaration files (*.d.ts) generated when building the lib project.

That's why your lib/tsconfig.json file must contain:

"declaration": true,

However, if you navigate to the definition of the log function in project-a/app.ts using F12 key in Visual Studio code, you'll be shown the correct typescript file. At least, if you have correctly setup your lib/tsconfig.json with:

"declarationMap": true,

I've create a small github repo demonstrating this example of project references with typescript:

https://github.com/thdk/TS3-projects-references-example

ThdK
  • 9,916
  • 23
  • 74
  • 101
  • Thank you! The only thing that's not working so well is: I'm trying to put my `.ts` files in `src`, and setup my `tsconfig` with `rootDir: './src'` and `outDir: `./lib`. Problem is when I want to reference one of these files in another project, the `import` statement is now all wrong. I'd do `import {something} from "../../dep/src/something.ts"`... But after build, that path is wrong, it should get the `js` file from `../../dep/lib/something.js`. But TypeScript is not doing this transformation. Any thoughts? – Arash Motamedi Mar 21 '19 at 20:18
  • 1
    @ArashMotamedi simply change your import to "../../dep/lib/something". This will only be available after your project reference ('dep') has been built. – ThdK Mar 22 '19 at 13:20
  • 7
    Great answer, but from what I'm seeing (I definitely could have messed something up), if I were to push "project-a" to production as a standalone project, the referenced lib would not be bundled with the code. It seems that you'd need to push the parent folder containing both project-a *and* the referenced lib. Perhaps webpack is necessary to "inline" the referenced lib? I may have a different use case than the OP...all I'm trying to do is have some loose typescript files that I want to be a "single sources of truth" rather than copy and paste them to various projects. – devuxer May 08 '20 at 23:37
  • @devuxer have you found a proper solution for your attempt? – da-chiller Nov 09 '20 at 11:16
  • @da-chiller, I haven't. The only solution I've found is to put all code in the same project. – devuxer Nov 10 '20 at 16:57
  • Does this help you? I've updated my answer with the optioal prepend option. https://www.typescriptlang.org/docs/handbook/project-references.html#prepend-with-outfile – ThdK Nov 11 '20 at 07:50
  • what if your `lib` folder has package.json with dependencies? – inside Nov 21 '20 at 16:03
  • You'll have to make sure they are installed before you run tsc -b. – ThdK Nov 24 '20 at 08:22
  • 2
    I get this error, when using `prepend` option: Cannot prepend project 'some/path/to/lib' because it does not have 'outFile' set. – User Rebo Dec 30 '20 at 21:30
  • 1
    @UserRebo Ah I'm sorry, prepend will probably only work when you are using a single outputFile for each project. Which only works for system or amd modules or when using modules resolution : none – ThdK Jan 03 '21 at 08:13
  • Thanks for this. This solution worked for me. However I wanted to add an `outDir` in the `tsconfig.json` for common folder so that all the bulit files are put there. But this broke the build for project-a and project-b. What do I need to do to make the build work again? – dragonfly02 Jul 11 '22 at 01:21
  • @dragonfly02 the common code will be included in both project a and b. If you don't want this behavior, remove the 'prepend' property from tsconfig files. Make sure to read the docs to understand what each setting is tsconfig does. – ThdK Jul 12 '22 at 03:37
  • @ThdK I do want the common to be referenceable by both projects. Currently, if I build project a or b, the `js`, `d.ts`, `d.ts.map` files for the common code is outputed to `lib` folder, whereas I want to output them to `lib/dist` folder leaving only ts files in the `lib` folder. When setting `outDir` for the common code to be `./dist`, project a and b don't build complaining about not being able to find referenced module from the commone code. – dragonfly02 Jul 13 '22 at 11:09
  • silly little harmless thing, projects that are referenced *dont*, in fact, need to have a `references` empty array. i say "harmless" because it doesn't affect the output either way – bdotsamir Aug 08 '22 at 01:44
  • Unluckily `prepend` is now deprecated: https://github.com/microsoft/TypeScript/issues/51909 – JoshThunar Apr 23 '23 at 18:53
  • @dragonfly02 is right, if there's an outDir set in a and b e.g. "outDir" : "dist" the built files will be in project-a/dist/ and project-b/dist/ but running node project-a/dist/app-a.js will throw a 'module not found error' as app-a.js is importing relative to it's .ts file location ('../common/index') but it should now be '../../common/index' as app-a.js is not in the project-a folder but project-a/dist – Alexandru Antochi Jun 21 '23 at 22:53
33

This can be achieved with use of 'paths' property of 'CompilerOptions' in tsconfig.json

{
  "compilerOptions": {
    "paths": {
      "@otherProject/*": [
        "../otherProject/src/*"
      ]
    }
  },
}

Below is a screenshot of folder structure.

enter image description here

Below is content of tsconfig.json which references other ts-project

{
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./tsc-out",
    "sourceMap": false,
    "declaration": false,
    "moduleResolution": "node",
    "module": "es6",
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2017",
      "dom"
    ],
    "paths": {
      "@src/*": [ "src/*" ],
      "@qc/*": [
        "../Pti.Web/ClientApp/src/app/*"
      ]
    }
  },
  "exclude": [
    "node_modules",
    "dist",
    "tsc-out"
  ]
}

Below is import statement to reference exports from other project.

import { IntegrationMessageState } from '@qc/shared/states/integration-message.state';
Oleg Polezky
  • 1,006
  • 14
  • 13
  • Are you sure [this](https://stackoverflow.com/q/43281741/264031) is only valid when it's sharing within the *same* (tsconfig) project? – Nicholas Petersen Dec 11 '18 at 02:36
  • 2
    @NicholasPetersen, I use this approach with two different tsconfig files in two different folders – Oleg Polezky Dec 11 '18 at 03:36
  • Ok, interesting, I tried it and it wasn't working, so I figured this didn't actually work across projects. Maybe I need to re-try it. Would you be so kind to share a fuller project structure so I could try to duplicate this in a simple hello-world way? – Nicholas Petersen Dec 11 '18 at 03:40
  • 1
    @NicholasPetersen, please see attached screenshot of folder structure, tsconfig.json and import statement – Oleg Polezky Dec 11 '18 at 05:49
  • 2
    Thank you @Oleg! That was helpful. I think my problem was this: The initial import (`import { IntegrationMessageState } ...`) seemed to work, but when I called members on the imported type, even though the type was recognized, none of its members were available, so I thought it was failing. Well duh, I was accessing directly on the type and not on a `new` instance of it (or call prototype property). So I think it had been working. FYI: Not sure the exact difference, but I ended up using the new project reference route: https://www.typescriptlang.org/docs/handbook/project-references.html – Nicholas Petersen Dec 11 '18 at 23:25
  • @NicholasPetersen, in my project I can access any nested objects of imported object. Maybe something wrong (special) with your particular classes? BTW, thank you for the link of new typescript 3.0 feature – Oleg Polezky Dec 12 '18 at 06:31
  • Most useful SO answer I've seen in a while – S.. May 17 '19 at 13:53
  • For some reason this doesn't work when importing shared enum types, any idea why? In that case it gives the error: `Module not found: You attempted to import /shared/declarations which falls outside of the project src/ directory. Relative imports outside of src/ are not supported. You can either move it inside src/, or add a symlink to it from project's node_modules/.` – S.. May 17 '19 at 19:28
  • I solved relative imports issue by adding one more path. project1 tsconfig.json paths: { "@prj1/*": [ "src/*" ], } there are imports in project1 like import { Something } from '@prj1/some-path'; project2 tsconfig.json paths: { "@prj2/*": [ "src/*" ], "@prj1/*": [ "../Project1/src/*" } Project2 is not allowed to have allias @prj1 – Oleg Polezky May 19 '19 at 03:27
  • 1
    have anyone tried this for node.js? where `shared` folder would have its own package.json with modules? – inside Nov 21 '20 at 15:58
  • Thanks, this worked (mostly)! I use a directory for Typescript interfaces which are used by my frontend and my backend. I defined `"paths": {"@interfaces/*": ["../interfaces/*"]}` in frontend's and backend's `tsconfig.json`. Two caveats: 1. VS Code would only stop complaining after a restart. 2. While I could do `import { MyInterface } from '@interfaces/my.interface'`, I could not define an `interfaces/index.ts` and then do `import { MyInterface } from '@interfaces'`. Somehow the compiler would not accept that. – Elias Strehle May 27 '21 at 15:18
10

I think that @qqilihq is on the right track with their response - Although there are the noted potential problems with manually maintaining the contents of a node_modules directory.

I've had some luck managing this by using lerna (although there are a number of other similar tools out there, for example yarn workspaces seem to be somewhat similar, although I've not used them myself).

I'll just say upfront that this might be a little heavyweight for what you're talking about, but it does give your project a lot of flexibility to grow in the future.

With this pattern, your code would end up looking something like:

/my-projects/
    /common-code/
        lib.ts
        tsconfig.json
        package.json
    /project-a/
        app.ts (Can import common-code)
        tsconfig.json
        package.json (with a dependency on common-code)
    /project-b/
        app.ts (Can import common-code)
        tsconfig.json
        package.json (with a dependency on common-code)

The general theory here is that the tool creates symlinks between your internal libraries and the node_modules directories of their dependant packages.

The main pitfalls I've encountered doing this are

  • common-code has to have both a main and types property set in its package.json file
  • common-code has to be compiled before any of its dependencies can rely on it
  • common-code has to have declaration set to true in its tsconfig.json

My general experience with this has been pretty positive, as once you've got the basic idea understood, there's very little 'magic' in it, its simply a set of standard node packages that happen to share a directory.

metric_caution
  • 121
  • 1
  • 7
  • After adding the projectB in package.json as 'projectB: file:../projectB'; and then in a class like this : import ClassB from 'projectB/package/ClassB'; – Smart Coder Apr 14 '23 at 19:45
0

It seems we've got a few options here:

(1) Put both projects in a mono repo and use answer given by @ThdK using TS project references

NOT GOOD if you don't want a mono repo

(2) Use Lerna - see answer by @metric_caution

NOT GOOD if you can't be bothered to learn Lerna / don't want to publish your shared files to npm

(3) Create a shared npm package

NOT GOOD if you don't want to publish your shared files to npm

(4) Put shared folders in a "shared" directory in project A and write a script to copy files in the shared folder from project A to project B's shared folder that is executed on a git push OR a script to sync the two folders.

Here the script can be executed manually when copying / syncing is required. The copying / syncing could also be done prior to a git push using husky and the shared files added to git automatically in the script.

Since I don't want a mono repo and I can't be bothered to publish an npm package for such a pathetic purpose I'm gonna go with option 4 myself.

danday74
  • 52,471
  • 49
  • 232
  • 283
0

Since I'm the only dev working on 2 projects which require some shared code folder I've setup a 2-way real time sync between the common code shared folders.

project A
 - shared/ -> 2-way sync with project B
 - abc/

project B
 - shared/ -> 2-way sync with project A
 - xyz/

It's a one-time quick setup but gives benefits like:

  • no hassle of configuring and managing mono-repo/multiple tsconfig
  • no build step to get latest code, works instantly
  • works with cloud build tools as shared code is inside repo instead of symlink
  • easier to debug locally as all files are within the project
  • I can further put checks on shared folder with commit hooks/husky and throw warnings etc.

And in-case if I want to collaborate with other team members, I can mention to use this sync tool as part of project setup.

E_net4
  • 27,810
  • 13
  • 101
  • 139
GorvGoyl
  • 42,508
  • 29
  • 229
  • 225
-5

In a similar scenario, where I also wanted to avoid the overhead of having to perform an NPM release, I went for the following structure (after lots of trial and error and failed attempts):

/my-projects/

  /node_modules/
    /my-lib/
      lib.ts
      tsconfig.json
      package.json

  /project-a/
    app.ts
    tsconfig.json

  /project-b/
    app.ts         
    tsconfig.json

The central idea is to move the shared stuff to a node_modules directory above the individual projects (this exploits NPMs loading mechanism, which would start looking for dependencies in the current directory and then move upwards).

So, project-a and project-b can now access the lib simply via import { Whatever } from 'my-lib'.

Notes:

  1. In my case, my-lib is actually only for shared typings (i.e. .d.ts files within a lib subdirectory). This means, I do not explicitly need to build my-lib and my my-lib/package.json looks as follows:

    {
      "name": "my-types",
      "version": "0.0.0",
      "private": true,
      "types": "lib/index"
    }
    
  2. In case my-lib contains runnable code, you’ll obviously need to build my-lib, so that .js files are generated, and add a "main" property to the package.json which exposes the main .js file.

  3. Most important: Despite its name, /my-projects/node_modules only contains custom code, no installed dependencies (they are actually in the individual projects project-a/node_modules and project-b/node_modules). This means, there’s an explicit git ignore setting, which un-ignores the /node_modules directory from being committed.

Is this a clean solution? Probably not not. Did it solve my issue? Yes. Am I happy to hear about improvement suggestions? Absolutely!

qqilihq
  • 10,794
  • 7
  • 48
  • 89
  • 13
    that sucks - `node_modules` is often deleted. You can't expect people or even yourself to remember that you customized `node_modules` folder. You shouldn't mess with it – Toolkit Jul 03 '18 at 09:43
  • 2
    @Toolkit Feel free to suggest a better solution. As we're using the root `node_module` **only** for custom modules, it worked fine within our team, and neither me nor my colleagues accidentally deleted anything. It includes a readme file which explains the motivation and an adjusted `.gitignore` file. Again -- better suggestions welcome. – qqilihq Jul 03 '18 at 13:31
  • I played with this solution and the `my-lib` doesn't compile because ts compiler doesn't look for imported npm modules in its local `node_modules`, seems like it looks in the upper one which is not a real one. If I rename `/my-projects/node_modules/` into `/my-projects/xxx/` the lib starts compiling fine. Any ideas? – Toolkit Jul 07 '18 at 17:22
  • 4
    @qqilihq I want to thank you for offering this solution, in light of the downvotes. I realize this stuff just absolutely stinks, so we have to try and adopt un-ideal solutions. Anyways, cheerio. – Nicholas Petersen Dec 11 '18 at 01:43
-13

I switched to deno. Code sharing in TypeScript projects is easy, at long last.

Alex Craft
  • 13,598
  • 11
  • 69
  • 133
  • 3
    is this really answering the original question???? you asked a very valid question, then answered with an alternative solution and marked it as "accepted". not fair. – Carlos Morales Apr 08 '21 at 09:06
  • 1
    @CarlosBarcelona you can't answer to wrong question, sometimes the question is wrong. Ok, I removed the accepted question :) – Alex Craft Apr 08 '21 at 09:16