TypeScript: TypeScript 3.9.7 doesn't auto import from dependancies of dependancies (3.6.3 does)

I have a “parent” app which is a base library that we use in other apps. We recently moved all the dependancies into the parent package.json, and removed them all from the children apps keeping only the parent. Making sure we had the right versions was cumbersome, and NPM would install all the dependancies of the parent app anyway so things work.

The problem we noticed is that we can only auto import from the parent (which is in the child app’s package.json), and not things like Angular or RxJS.

TypeScript Version: 3.9.7 (also 3.8.3, and 4.0.0-dev.20200803 with a VS Code plugin)

Search Terms: auto import package.json dependancies types

Code

I have created an example repo here: https://github.com/cjdreiss/ts-import-error

It contains the “parent” app, and a “child” app with instructions in the readme.

Parent package.json

"dependencies": {
    "@angular/animations": "~10.0.6",
    "@angular/common": "~10.0.6",
    "@angular/compiler": "~10.0.6",
    "@angular/core": "~10.0.6",
    "@angular/forms": "~10.0.6",
    "@angular/platform-browser": "~10.0.6",
    "@angular/platform-browser-dynamic": "~10.0.6",
    "@angular/router": "~10.0.6",
    "rxjs": "~6.5.5"
}

Child package.json

"dependencies": {
  "@cjdreiss/ts-import-error-parent" : ">0.0.0"
}

Component trying to import things from rxjs

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { ExampleComponent } from '@cjdreiss/ts-import-error-parent';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'child';
  test: Observable<any>;
  // test2: BehaviorSubject<any>; // won't auto import even though there is an rxjs import already...
  parent: ExampleComponent; // auto imports because @cjdreiss/ts-import-error-parent is in package.json

}

Expected behavior: Auto imports from things like rxjs and @angular/xxx should work

Actual behavior: Only auto imports from dependancies listed in package.json work (my library as an example). If we change the TypeScript version VS Code is using to 3.6.3 it can import it.

Non working import in 3.9.7 Aug-04-2020 16-33-16

Working import after switching the TS version to 3.6.3 Aug-04-2020 16-33-50

Related Issues: I believe this issue might be related (although it says its in Milestone 4.0.0), and there are other related issues in that https://github.com/microsoft/TypeScript/issues/37812

This seems to demonstrate the same issue: https://github.com/microsoft/TypeScript/issues/37187 which was closed as a duplicate of https://github.com/microsoft/TypeScript/issues/36042

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 9
  • Comments: 24 (6 by maintainers)

Most upvoted comments

This is very much the intentional behavior; dependencies of your dependencies are not part of the dependency contract and it’s fundamentally incorrect for us to encourage violating that assumption. You are always free to write the imports manually.

@RyanCavanaugh

TypeScript used to work with this use-case (having a shared framework). It no longer does. You guys inadvertently broke an important use case we and others were using in production. A use case that is important for enabling enterprise-level web development (which was one of the original goals of TypeScript…). Just because you philosophically disagree with it doesn’t change any of those facts.

Couldn’t the TypeScript compiler at least provide a compiler flag to opt-in to the old behavior?

“Writing imports manually” isn’t a solution when you have dozens of developers with varying degrees of expertise all utilizing the same common framework.

Typescript traverses up the directories and scans package.json files it finds. https://github.com/microsoft/TypeScript/blob/04205ca32c2b6e60a6c38cbf47961319566a8ea4/src/server/project.ts#L1903

This means if I create a dummy package.json inside src/ I can get auto-imports to work without messing with the project’s own package.json.

./package.json

{
  "name": "hello",
  "private": true,
  "dependencies": {
    "package-that-contains-dependencies": "^1.0"
  },
  "scripts": {
    "postinstall": "script-that-populates-dummy-package.json"
  }
}

./src/package.json

{
  "peerDependencies": {
    "__comment": "these dependencies are necessary for auto-import to work properly",
    "__comment2": "these come from the package 'package-that-contains-dependencies'",
    "axios": "*",
    "react-admin": "*",
    "@material-ui/core": "*",
    "@material-ui/icons": "*",
    "@material-ui/lab": "*",
    "react-router-dom": "*",
    "@types/react-router-dom": "*",
    "react-final-form": "*"
  }
}

The script that generates the dummy package.json:

// PackageManager is an abstraction for npm/yarn
const pm = new PackageManager(cwd);

const version = await pm.installedVersion(CORE_PACKAGE_NAME); // npm list $packageName -> then get version, or read package.json directly from node_modules/$packageName/package.json
const corePkg = await pm.info(CORE_PACKAGE_NAME, version);

logger.info("Generating a dummy package.json for auto-import suggestions");
const pkg = {
    notes: [
        "DO NOT DELETE OR MODIFY THIS FILE!",
        "These dependencies are necessary for Typescript auto-import suggestions to work",
        `This file will get updated after every install with dependencies from ${CORE_PACKAGE_NAME}`,
    ],
    peerDependencies: {
        ...Object.fromEntries(corePkg.injectableDependencies!.map((d) => [d, "*"])), // not all dependencies need to be added for import suggestions. injectableDependencies is a list of dependencies that we want users of the framework to have access to
    },
};
await writeJson(path.resolve(cwd, "src", "package.json"), pkg);

This comment is intended to summarize approaches to the problem identified in this ticket.

First, I’ll share that I too am running this problem. I have package A whose package.json lists peerDependencies B and C. I want consumers of package A to be able to npm install package A and have B and C installed automatically, this works great. However, I want them to also be able to use VSCode to autocomplete anything from A, B, or C. This does not work, only A will autocomplete. This thread and other threads explain very well that this is the intended behavior. (Note that in my use case these are private packages, but I think these principles apply the same.) And, to put a real-world use case to the problem I want to bundle a suite of NestJS and related packages together as a starting point for building applications, conceptually similar to how Drupal has distros. A “bundle” package would allow distributing a single package that does the hard work of specifying the right version compatibility of all packages in the bundle.

I see 5 umbrella approaches to this problem. In the examples below I’ll refer to these fake package names:

  • A = @vendor1/package1
  • B = @vendor2/package1
  • C = @vendor3/package1

1 - Don’t import dependencies of dependencies

In this scenario, you change the assumptions. Instead of expecting consumers of package A to be able to autocomplete from B and C, you say that consumers of package A can only autocomplete package A. It would be the responsibility of A to expose any functionality that B or C provide. I think this perspective was succinctly phrased in @RyanCavanaugh’s comment.

CON: A “bundle” package isn’t valid in this scenario. If a consumer needs functionality from B, then package A must expose that functionality, e.g. using adapter pattern.

I don’t have an example of how this would work; it would be highly specific to the code in A, B, and C.

2 - Directly list dependencies

Have consumers of package A list B and C as direct dependencies, e.g. using peerDependencies. See @abdusco’s comment. To avoid version conflicts between your project’s specific versions of B and C with package A’s versions of B and C, consider using loose specificity in your project, e.g. "@vendor1/package1": "*".

CON: Bubbling up dependencies B and C requires manual upkeep or custom tooling, and may break lockfiles in CI tooling (see @abdusco’s comment).

Expand to see example

The package.json for your project:

{
  "dependencies": {
    "@vendor1/package1": "^1.0.0"
  },
  "peerDependencies": {
    "@vendor2/package1": "*",
    "@vendor3/package1": "*"
  }
}

The package.json for package A:

{
  "peerDependencies": {
    "@vendor2/package1": "^1.0.0",
    "@vendor3/package1": "^1.0.0"
  }
}

3 - Add a dummy package.json solely for autocomplete

Have consumers of package A list B and C as direct dependencies in a dummy package.json that is within the ts include path, e.g. src/package.json. See @abdusco’s comment.

CON: Bubbling up dependencies B and C requires manual upkeep or custom tooling, and the fact you have multiple package.json files may be confusing.

Expand to see example

Your project’s directory structure:

|-- package.json
|-- src
	|-- package.json # This is a dummy, not read by npm but is read by ts / vscode for autocomplete
	|-- somecode.ts

The package.json in the root of your project:

{
  "dependencies": {
    "@vendor1/package1": "^1.0.0"
  }
}

The dummy package.json in src/:

{
  "peerDependencies": {
    "@vendor2/package1": "*",
    "@vendor3/package1": "*"
  }
}

IMPORTANT! If your project’ structure is more complicated, e.g. a monorepo with NestJS or NX, you will need a dummy package.json in each directory like so:

|-- package.json
|-- apps/
	|-- package.json # This dummy is used for all sub-directories
	|-- app1/
		|-- src/
			|-- somecode.ts # When editing this file, autocomplete will read from "apps/package.json"
	|-- app2/
		|-- src/
			|-- somecode.ts # When editing this file, autocomplete will read from "apps/package.json"
|-- libs/
	|-- package.json # This dummy is used for all sub-directories
	|-- lib1/
		|-- src/
			|-- somecode.ts # When editing this file, autocomplete will read from "libs/package.json"
	|-- lib2/
		|-- src/
			|-- somecode.ts # When editing this file, autocomplete will read from "libs/package.json"

4 - Re-export using type definitions

Have consumers of package A use a type definition that re-exports types from sub-dependencies B and C. See @abdusco’s comment.

CON: Bubbling up dependencies B and C requires manual upkeep or custom tooling.

Expand to see example

The tsconfig.json for your project:

{
  "compilerOptions": {
    "paths": {
      "@vendor2/*": [
        "typings/vendor2/*"
      ],
      "@vendor3/*": [
        "typings/vendor3/*"
      ],
  }
}

The type definition for B at typings/vendor2/index.d.ts:

export * from '@vendor2/package1';
# Note: if there were multiple packages you could simply add them as long as they are from @vendor2.
export * from '@vendor2/package2';

The type definition for C at typings/vendor3/index.d.ts:

export * from '@vendor3/package1';

5 - Package inheritance

Have consumers of package A use package inheritance. A StackOverflow thread points out that npm does not support package inheritance and is unlikely to do so. However, there are a few alternatives that could be explored to see if they are valid for your situation:

CON: NPM does not support inheritance, so custom tooling is required or switching dependency managers.

For examples, look at the relevant tools listed above.


I hope this is a helpful and accurate summary; there are a lot of related tickets that I have spent considerable time wading through and I think this is the proper spot to comment. I also hope this saves others time and inspires identification of a simpler solution. If I have said anything in error please provide correction and I will update.

Edit 2022-07-08 15:47 ET: Expanded from 3 to 5 approaches.

This is very much the intentional behavior; dependencies of your dependencies are not part of the dependency contract and it’s fundamentally incorrect for us to encourage violating that assumption. You are always free to write the imports manually.

Looks like TS team should talk with other well-known teams, like framework devs to find out a way, that will still provide import suggestions (at least for frameworks classes and objects). Like an option or separate feature.
Just disabling such feature, is not a correct way. Huge step back.

@abdusco we didn’t find a work around. We went back to using full, “normal”, package.json files in our consuming apps and built a CLI upgrade tool to help manage the package.json files in our apps during upgrades.

You’re solution seems pretty clever though, if we get tired of the “normal” way of doing it in the future, maybe I’ll try this but for now we’re going to stick with the usual way of having a proper package.json file.

Is it possible for the TS team to add some option to specify which dependencies it should use to resolve auto-imports for? That way you could opt in to the old behavior for a specific dependency.

Anyone who builds a “base class library” type situation used to have this scenario work and it’s now broken. That seems like a major regression to me…