TypeScript: Duplicate type declarations with npm link

Using TypeScript 1.7.3.

Suppose I have the below npm packages. The declaration files are generated by TypeScript compiler, and referred to from the other packages by means of the way described here.

package-a

ts src:

export default class ClassA {
  private foo: string;
  bar: number;
}

ts declaration:

declare class ClassA {
  private foo;
  bar: number;
}
export default ClassA;

package-b (depends on package-a):

ts src:

import ClassA from 'package-a';

namespace ClassAFactory {
  export function create(): ClassA {
    return new ClassA();
  }
}
export default ClassAFactory;

ts declaration:

import ClassA from 'package-a';

declare namespace ClassAFactory {
  function create(): ClassA;
}
export default ClassAFactory;

package-c (depends on package-a and package-b):

ts src:

import ClassA from 'package-a';
import ClassAFactory from 'package-b';

let classA: ClassA;
classA = ClassAFactory.create(); // error!!

The last line causes an error during compilation:

error TS2322: Type 'ClassA' is not assignable to type 'ClassA'.
Types have separate declarations of a private property 'foo'.

When I remove the line private foo; from the declaration of package-a, TypeScript does not emit any error. However this workaround is a bit painful.

I understand that exposing private properties to declaration is by design (https://github.com/Microsoft/TypeScript/issues/1532). I think TypeScript should ignore private properties when compiling variable assignment. Or is there any better workaround for this?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 42
  • Comments: 147 (33 by maintainers)

Commits related to this issue

Most upvoted comments

I’ve just merged a change which will attempt to detect duplicate packages based on their name and version, and use only one. Please try it out using typescript@next when that is next published.

Fair enough, but someone else might 😉

I’ve reproduced this error.

mkdir a; cd a
npm install rxjs
echo 'import * as rx from "rxjs"; export const myObservable: rx.Observable<number>;' > index.d.ts
echo '{ "name": "a" }' > package.json

cd ..; mkdir b; cd b
npm install rxjs
npm link ../a
echo 'import * as rx from "rxjs"; import * as a from "a"; const x: rx.Observable<number> = a.myObservable;' > index.ts
tsc index.ts --target es6 --moduleResolution node

Since there are two installations of rxjs, we get:

index.ts(1,59): error TS2322: Type 'Observable<number>' is not assignable to type 'Observable<number>'.
  Property 'source' is protected but type 'Observable<T>' is not a class derived from 'Observable<T>'.

Still running into this issue with typescript@2.8.1. Also tried typescript@next and had the same issue.

The root of the problem seems to be that linked packages still reference the type definitions in their own local node_modules rather than using the typings from the node_modules into which they are linked, when possible. That combined with the fact that:

  1. globals cannot be re-defined
    • this causes the compiler to complain when the global is defined both in the parent project’s node_modules as well as the node_modules in the linked package
  2. classes that are otherwise identical cannot be assigned to one another
    • this causes the compiler to complain when trying to assign a variable whose type is a class from the parent project’s node_modules to a value returned by the linked package whose type is defined in its own node_modules

I’ve been able to work around this issue using the paths config variable. For modules whose definitions come from @types/*, as suggested here, you can simply use:

"paths": {
  "*": ["node_modules/@types/*", "*"]
}

In the case where you run into this issue with a package that comes bundled with type definitions which define classes or globals, you have to add them manually. For example, rxjs:

"paths": {
  "rxjs": ["node_modules/rxjs"],
  "rxjs/*": ["node_modules/rxjs/*"]
}

tsconfig.json exposes a path mapping, add duplicated dependencies into paths, so it will be loaded from the right node_modules instead of linked one.

{
  "compilerOptions": {
    "baseUrl": ".", // This must be specified if "paths" is.
    "paths": {
      "@angular/common": ["../node_modules/@angular/common"],
      "@angular/compiler": ["../node_modules/@angular/compiler"],
      "@angular/core": ["../node_modules/@angular/core"],
      "@angular/forms": ["../node_modules/@angular/forms"],
      "@angular/platform-browser": ["../node_modules/@angular/platform-browser"],
      "@angular/platform-browser-dynamic": ["../node_modules/@angular/platform-browser-dynamic"],
      "@angular/router": ["../node_modules/@angular/router"],
      "@angular/http": ["../node_modules/@angular/http"],
      "rxjs/Observable": ["../node_modules/rxjs/Observable"]
    }
  }
}

I ended up not using npm link, and this does not matter any more for me.

For easier usage, you can also set the paths like this:

{
    "compilerOptions": {
        "baseUrl": ".", // This must be specified if "paths" is.
        "paths": {
            "@angular/*": ["../node_modules/@angular/*"],
            "rxjs/*": ["../node_modules/rxjs/*"]
        }
    }
}

Sorry for bugging again with this issue, but it is a serious drag on our project not being able to do npm link while we are making changes. I would love to help out with a PR if one of the current TypeScript contributors could give me a little guidance on where to start looking in the codebase.

I just ran into this issue and it is a major problem for us because we use try to split up our back end into many small libraries. During development, we often need to npm link our repos. The specific issue I ran into which prompted me to find this is the use of rxjs Observables and interfaces:


// in repo A
export class HttpAdapter {
    request(url: string, options?: HttpRequestOptionsArgs): Observable<HttpResponse> {
        return Observable.of({});
    }
}

// in repo B
export class HttpRequestAdapter implements HttpAdapter {
    request(url: string, options?: HttpRequestOptionsArgs): Observable<HttpResponse> {
        return Observable.of({});
    }
}

This works if I don’t npm link, but when I do, I get:

Error:(10, 14) TS2420:Class 'HttpRequestAdapter' incorrectly implements interface 'HttpAdapter'.
  Types of property 'request' are incompatible.
    Type '(url: string, options?: HttpRequestOptionsArgs) => Observable<HttpResponse>' is not assignable to type '(url: string, options?: HttpRequestOptionsArgs) => Observable<HttpResponse>'.
      Type 'Observable<HttpResponse>' is not assignable to type 'Observable<HttpResponse>'.
        Property 'source' is protected but type 'Observable<T>' is not a class derived from 'Observable<T>'.

I’m using typescript 2.0.3 and I’m seeing this error with Observable as described above, e.g.

Type 'Observable<Location[]>' is not assignable to type 'Observable<Location[]>'. Property 
            'source' is protected but type 'Observable<T>' is not a class derived from 'Observable<T>'.

This is a pretty brutal problem that will only affect more complex codebases, and it seems impossible to work around without taking drastic measures, so I hope I can convince you all to give it the attention it deserves. 😄

It’s most noticeable in cases where you have an app that depends on Dependency A, Dependency A depends on Dependency B and vends objects that contain types from Dependency B. The app and Dependency A both npm link Dependency B and expect to be able to import types from it and have them describe the same thing.

This results in deep error messages, and I’m on the verge of going through and eliminating all of the private and protected properties in my libraries because I’ve already lost so much time to this:

TSError: ⨯ Unable to compile TypeScript
tests/helpers/test-application.ts (71,11): Argument of type '{ initializers: Initializer[]; rootPath: string; }' is not assignable to parameter of type 'ConstructorOptions'.
  Types of property 'initializers' are incompatible.
    Type 'Initializer[]' is not assignable to type 'Initializer[]'.
      Type 'Application.Initializer' is not assignable to type 'Application.Initializer'.
        Types of property 'initialize' are incompatible.
          Type '(app: Application) => void' is not assignable to type '(app: Application) => void'.
            Types of parameters 'app' and 'app' are incompatible.
              Type 'Application' is not assignable to type 'Application'.
                Types of property 'container' are incompatible.
                  Type 'Container' is not assignable to type 'Container'.
                    Types of property 'resolver' are incompatible.
                      Type 'Resolver' is not assignable to type 'Resolver'.
                        Types of property 'ui' are incompatible.
                          Type 'UI' is not assignable to type 'UI'.
                            Property 'logLevel' is protected but type 'UI' is not a class derived from 'UI'. (2345)

Really appreciate you all looking into this; thank you!

Can confirm this is working for me now with typescript@2.6.0-dev.20170908. Big thanks to @andy-ms - this is a game changer!

I wanted to provide an update. From an offhand exchange I had with @mhegazy, here’s what we’re thinking.

  • One solution that we’re considering the the concept of distinguishing packages based on their version as well as their resolution name. I don’t have full details for what would be involved in this.
  • Another is to just fully expand the path to get the “true” identity of a symlink. I believe this is simpler, but more limited when it comes to dealing with the same package at different versions, but helps solve a decent number of cases.

@heruan I will try to set that up.

One, FYI, though. I think I may have found a work around. The problem is resolved if I npm link rxjs in both ProjectA and ProjectB. This sort of makes sense because in that case, both ProjectA and ProjectB are using the same exact rxjs files. Without that, they are technically using different files (even though the same version):

If you just npm link ProjectA from ProjectB, then:

  • ProjectB is pointing to node_modules/rxjs
  • ProjectA exists as a symlink in node_modules/ProjectA and the rxjs it references is at node_modules/ProjectA/node_modules/rxjs

But if you npm link rxjs in both then both of those rxjs references will be symlinked to the same exactly global npm location.

Anyways, this is obviously still not ideal, but at least something that can move us forward.

Also…not sure if this is relevant or matters (will see once I set up the test project), but my two libs (i.e. ProjectA and ProjectB) are actually private npm repos.

The issue I’m seeing in my Lerna repo is somewhat involved, so I made a stripped-down version of it at https://github.com/seansfkelley/typescript-lerna-webpack-sadness. It might even be webpack/ts-loader’s fault, so I’ve filed https://github.com/TypeStrong/ts-loader/issues/324 over there as well.

Thanks for all the work in analyzing and documenting this issue. We’re having the same problem in some of our code bases. We ported some projects to properly use package.json dependencies but are now seeing this when using npm link during development.

Is there anything I can help to solve this issue?

Also ran into this when using a react project that has @types/react installed and linking it to an app that also uses @types/react

@grofit I think the problem in your situation is that you have two declarations of a module, where what you need is a module and an augmentation. You should be able to do mongo-promisification.d.ts like this:

import * as mongodb from "mongodb"; // import the real mongo

// now augment it
declare module "mongodb" {
    // new stuff...
}

Without the import, you are in an ambient context and are writing a new declaration, not an augmentation.

(Handbook ref)

This is a huge blocker when working with monorepos. Really hope this will no longer delayed and finds its way into 2.5.

Edit: 😮

image

Peer dependencies were invented for a situation where a plug-in wants to say what it goes with. They sort of work okay for that case. But they cause a lot of problems if people start converting all their dependencies to peer dependencies, hoping to avoid “npm install” duplication.

We spent months banishing them from our internal git repos. When used as I think you’re suggesting, peer dependencies allow you to solve a side-by-side versioning problem by trading it for several new problems:

  • the entire tree is inverted, i.e. every package now becomes responsible for taking a hard dependency on packages that used to be indirect dependencies, in many cases without any idea what it is for

  • if a peer dependency is removed, these hard dependencies will probably never get removed

  • package authors are tempted to use broad ranges for their peer version patterns, claiming to work with versions that they never actually tested against; broken builds suddenly become the consumer’s problem

Hi guys, Someone found a solution ?? I have the same issue when I link a project with another with RxJS. Thanks 😉

@xogeny I know, I’m trying too, I’d love to see it resolved correctly 😄 I read the linked issues, but it they are all designed to resolve the realpath of a symlink which implies if you have two (real) files they’ll still conflict because they’ll resolve to different locations. Which is what happens when you npm link from one project into another as both would have their own dependencies that can differ with re-exported symbols from the npm linked package.

Edit: I can confirm, all the issues are because of two files. npm link would trigger it because it’s simple to have a dependency in a repo that you just linked that is the same dependency as in the project you linked to. A simple repro would be to do an npm install of the same dependency at two different levels of an application and watch them error out.

image

FYI, I’ve also ended up here as a result of using npm link and getting this error. Has anybody found a workaround for this?

there are actually two files on disk with two declarations of ClassA. so the error is correct. but we need to consider node modules when we compare these types. this issue has been reported before in https://github.com/Microsoft/TypeScript/issues/4800, for Enums we changed the rule to a semi-nominal check. possibly do the same for classes.

This still doesn’t work for me when I’m trying to link 2 packages, each having a dependency on rxjs. I’m using rxjs@5.5.6 and typescript@2.6.1. Both packages are using the exact same version. Does anyone have a workaround for this?

My team has hit this problem pretty frequently when linking our own internal libraries during development.

Recently we found a tool named yalc which mitigates this really well and actually makes for a pretty good dev loop. (As a tl;dr instead of linking the whole package it runs the prepublish scripts and copies the result to a linked folder).

Don’t think this has been mentioned yet, but the super-shorthand way of working around this issue is:

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "*": ["node_modules/*", "*"]
        }
    }
}

The paths entry above basically says: for any module, first look it up in the node_modules folder in the root of the project, then fallback to the normal rule (recursive walk up the directories from where the import has occurred).

(Please correct me if I’m wrong, I’m still a relative newcomer to TS).

The entry in paths appears to be relative to the baseUrl. So, if you set baseUrl to be in a subfolder, you need to change your paths definition accordingly. E.g.:

{
    "compilerOptions": {
        "baseUrl": "./src",
        "paths": {
            "*": ["../node_modules/*", "*"]
        }
    }
}

@mikehutter we’ve recently added some guidance in Angular CLI for working with linked libs, and it seems like in your case you’re missing the TypeScript paths configuration for RxJs in your consumer app. See https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/linked-library.md for more info.

@mikehutter In your project which uses your library (not in the library itself), do something like this in your tsconfig:

    "paths": {
      "rxjs/*": ["../node_modules/rxjs/*"]
    },

You may need some slight adjustment to this depending on where your tsconfig and source code result.

(Like others in this thread, I’d love to see some TypeScript improvement which makes this unnecessary.)

@nicksnyder you should be able to do something like this:

cd libA/node_modules/@types/node; npm link
cd libB; npm link @types/node
cd libC; npm link @types/node

That way, B & C are pointing at the exact same files that A is.

Also, note that this is not just a problem with npm link, you will get this problem too on production builds, when your shared dependencies point to a different version.

ie. ProjectA needs rxjs@4.9.1 and ProjectB uses rxjs@4.9.2

When you install ProjectA as a dependency in ProjectB you will too have duplicated types, since there will be for example two Observable declarations, one in node_modules/rxjs and one in node_modules/project_a/node_modules/rxjs

You can get around this by allowing rxjs version in ProjectA to be something like ~4.9.0, so that npm install doesn’t need to download its own version, and will instead use ProjectB version. But keep in mind that this is not only a development workflow issue.

@mhegazy Well I started getting these errors like the one above (except I was using Observable from rxjs, i.e., "Type ‘Observable’ is not assignable to type ‘Observable’). This, of course, seemed odd because the two I was referencing Observable from exactly the same version of rxjs in both modules. But where the types “met”, I got an error. I dug around and eventually found this issue where @kimamula pointed out that if you use npm link, you’ll get this error. I, like others, worked around this (in my case, I created a duplicate interface of just the functionality I needed in one module, rather than references rxjs).

Does that answer your question? I ask because I don’t think my case appears any different than the others here so I’m not sure if this helps you.

I’m using Lerna which symlinks packages around and seeing the issue there as well. Typescript version 2.0.3.

Unfortunately Lerna and its symlinks are a hard requirement, so I used this nasty workaround to get this to compile fine while still being properly type-checkable by consumers:

export class MyClass {
  constructor(foo: Foo) {
    (this as any)._foo = foo;
  }

  get foo() {
    return (this as any)._foo as Foo;
  }
}

The class is very small so it wasn’t that arduous, and I don’t expect it to change really ever, which is why I consider this an acceptable workaround.

@dakaraphi Thanks! It looks like the error is due to vscode.d.ts being written as a global, ambient declaration rather than as an external module. I’ve created Microsoft/vscode-extension-vscode#90.

I’m running the latest test build (Windows 10 64 bit) and this doesn’t seem to be fixed for me.

Reproduction

Structure

a/
  index.ts
  package.json
b/
  index.ts
  package.json

Run

cd a
npm link
cd ../b
npm link a

a/index.ts

import { Observable } from 'rxjs/Observable';

export class Foo {
  public bar: Observable<any>;
}

b/index.ts

import { Foo } from '@rxjs-test/a';
import { Observable } from 'rxjs/Observable';

const baz = new Foo();

function qux (quux: Observable<any>) {}

// TypeError
qux(baz.bar);

Run

b>tsc -v
Version 2.6.0-dev.20170826

b>tsc index.ts
index.ts(11,5): error TS2345: Argument of type 'Observable<any>' is not assignable to parameter of type 'Observable<any>'.
  Property 'source' is protected but type 'Observable<T>' is not a class derived from 'Observable<T>'.

The types imported from the different versions of the libraries (and the corresponding @types packages) can be both equal and different (if the library updates, the type definition should update as well to reflect the changes).

It’s not just about npm link which can be replaced or not used, but about having multiple versions of the same @types/xxx package installed at the same time, often pulled in from dependencies which provide TypeScript declarations that depend on standard declarations, as me and other people reported. The number of such packages will grow with more adoption of TypeScript in the Node community.

That’s a good insight. Have you looked at pnpm? It is a small step forward from the current node_modules design, but a very effective one if I understand it correctly.

To be honest, I’m not even sure if this should be “fixed” in Typescript, as there also a problem with plain JavaScript: The instanceof operator will return false if the object’s class differs from the provided class.

Thus, I’d rather look forward to a better alternative to npm link that maybe hooks into require(), first.

@leovo2708 module ‘foo’:

import { Observable } from 'rxjs'
export function foo() {
  return Observable.of('foo')
}

client code in other module, that depends on module ‘foo’:

import { Observable } from 'rxjs'
import { foo } from 'foo'

Observable.of(foo()) // wrap the returned Observable
.forEach(res => console.log(res))

@mhegazy I am on Version 2.2.0-dev.20161129 and still having the issue. The specific issue is that I have one project (let’s call it ProjectA) that contains an “interface” (using a class, but that is so I can use the class as a token for Angular 2 DI) as follows:

export class ServerAdapter {
    start(opts: ServerOptions): Observable<any> {
        return null;
    }
}

Then in a completely separate project (let’s call it ProjectB) that has a class which implements the interface from the first project like this:

export class RestifyServerAdapter implements ServerAdapter {
    start(opts: ServerOptions): Observable<any> {
        let server = restify.createServer();
        this.addPreprocessors(server);
        this.addRequestHandler(server, opts);
        return this.startServer(server, opts);
    }

   // more stuff here that is not relevant to this issue
}

When I do a normal typescript compile for ProjectB it works fine. But if I npm link ProjectA from the ProjectB root directory and then run tsc again I get:

Types of property 'start' are incompatible.
    Type '(opts: ServerOptions) => Observable<any>' is not assignable to type '(opts: ServerOptions) => Observable<any>'. Two different types with this name exist, but they are unrelated.
      Type 'Observable<any>' is not assignable to type 'Observable<any>'. Two different types with this name exist, but they are unrelated.
        Property 'source' is protected but type 'Observable<T>' is not a class derived from 'Observable<T>'.

i have a workaround which works great for commandline, but visual studio still gets all messed up: https://github.com/Microsoft/TypeScript/issues/11107#issuecomment-254003380

my new workaround for Windows + Visual Studio 2015 is to robocopy my xlib Library src and dist folder into the node_modules\xlib\src and node_modules\xlib\dist folders of the consuming project.

here is the significant part of my robocopy batch file script if anyone wants it:

:rerunloop
    @echo watching for changes to project files..............  (Ctrl-C to cancel)

    @rem xlib --> blib and slib
    @robocopy .\xlib\src .\blib\node_modules\xlib\src *.*  /MIR /NJH /NJS /NDL /XD .git
    @if NOT "%errorlevel%" == "0" (
        @rem copy occured, so copy both

        @robocopy .\xlib\dist .\blib\node_modules\xlib\dist *.*  /MIR /NJH /NJS /NDL /XD .git   
        @robocopy .\xlib\src .\slib\node_modules\xlib\src *.*  /MIR /NJH /NJS /NDL /XD .git     
        @robocopy .\xlib\dist .\slib\node_modules\xlib\dist *.*  /MIR /NJH /NJS /NDL /XD .git

        @rem  set the src dirs readonly
        @attrib +R .\blib\node_modules\xlib\src\*  /S /D
        @attrib +R .\slib\node_modules\xlib\src\*  /S /D
    )
    @timeout /t 1 /nobreak > NUL
@goto rerunloop