Description
TypeScript Version:
2.0.3
Actual behavior:
This issue is a continuation of an earlier issue involving symlinks (#8346).
Suppose that library-a is an NPM package that exports a TypeScript class called MyClass, and two other packages consume this class. If those packages find the MyClass.d.ts definition via two different file paths on disk, and if MyClass contains a private member, then the compiler reports an error because it does not trust that the implementations are interchangeable.
This makes sense in general, however there are everyday NPM usage scenarios where provably equivalent definitions must unavoidably have different file paths. In this situation the TypeScript error is not reasonable and makes life difficult for developers.
Code
Unlike the earlier issue #8346, this repro does not involve symlinks at all. In the attached CompilerBug.tar.gz, the different file paths arise as follows:
-
library-a 1.0 exports MyClass
-
library-d exports MyLibrary like this:
import { MyClass } from '@local/library-a'; export default class MyLibrary { public static getMyClass(): MyClass { return undefined; } }
-
library-e exports YourLibrary like this:
import { MyClass } from '@local/library-a'; export default class YourLibrary { public static setMyClass(myClass: MyClass): void { } }
-
application's package.json imports both libraries and also a library-b which indirectly depends on version 2.0 of library-a
"dependencies": { "typescript": "^1.8.10", "@local/library-b": "1.0.0", "@local/library-d": "1.0.0", "@local/library-e": "1.0.0" }
-
As a result, the node_modules folder must unavoidably create two copies of library-a@1.0.0 (even after you run "npm dedupe" with npm version 3.0!):
C:\NpmInstallRepro\application>tree /a +---lib +---node_modules | +---.bin | +---@local | | +---library-a | | | \---lib | | +---library-b | | | \---lib | | +---library-d | | | +---lib | | | \---node_modules | | | \---@local | | | \---library-a | | | \---lib | | \---library-e | | +---lib | | \---node_modules | | \---@local | | \---library-a | | \---lib | \---typescript | +---bin | \---lib \---src
-
application tries to do this:
import { MyLibrary } from '@local/library-d';
import { YourLibrary } from '@local/library-e';
YourLibrary.setMyClass(MyLibrary.getMyClass());
-
The error looks like this:
src/index.ts(5,24): error TS2345: Argument of type 'MyClass' is not assignable to parameter of type 'MyClass'. Types have separate declarations of a private property '_data'.
-
There are two physical files (containing equivalent definitions):
CompilerBug\application\node_modules@local\library-d\node_modules@local\library-a\lib\MyLibrary.d.ts
CompilerBug\application\node_modules@local\library-e\node_modules@local\library-a\lib\MyLibrary.d.ts
Running the Repro
Extract the attached CompilerBug.tar.gz on a Windows PC and follow the steps in Instructions.txt. Note that this repro requires you to install sinopia, which is a private NPM server where you can publish the test packages. (Also note that the batch files are very simple and intended to be run multiple times, so the first time they will report some errors when they try to delete output folders that don't exist yet.)
Expected behavior:
I believe the compiler should determine that the MyLibrary.d.ts files are equivalent and treat them as the same class. If we're going to use classes in our public API at all, it seems that this must be supported, otherwise developers will have to do awkward "as any" workarounds when they randomly happen to hit a certain edge case for "npm install." In some cases (e.g. a class trying to extend a base class from a separate package) there is no reasonable workaround.
One idea would be for the compiler to examine the containing package.json for each file path, and check whether the version numbers are exactly the same. If there is a concern about a package being locally modified, "npm install" injects other fields in package.json that the compiler could use to determine whether the package is pristine or not.
In the past, it was suggested that we might be able to solve this using tsc path mapping. I'm not sure that this is the right approach. In the above example, we are not trying to remap the "application" project; we actually need to remap its indirect dependency "node_modules/@local/library-e" to find MyClass.d.ts in the "node_modules/@local/library-d" directory. Configuring the application to let us micromanage the module resolution in this way feels wrong. Basically our build scripts would be completely circumventing the module resolution algorithm that the compiler is supposed to provide for us.
If you could provide a compiler option to completely suppress TS2345, that would be a reasonable (temporary) workaround for us. This error is detecting a fairly obscure situation that is less likely to occur than the above scenario.