How to have TSLint resolve indirect typing dependencies with Yarn workspaces?
Asked Answered
E

1

13

Context

Yarn workspaces provide for a convenient way to depend on packages in a mono-repo. When package A depends on package B, the interfaces etc. defined in package B are appropriately resolved in package A.

Problem

I am facing an issue where if package B is depending on an external library, but that external library lacks typings, and therefore package B has created its own some-library.d.ts file. When using tslint to lint package A, this custom definition file is resolved properly for expressions in package B, but not for expressions in package A that are working with types from package B.

I've pushed a simplified example of this problem here:

https://github.com/tommedema/tslint-yarn-workspaces

The core of it is as follows.

packages/a/src/index.ts

// tslint:disable:no-console

import { someDependedFn } from 'b'

export const someDependingFn = (): void => {
  const someNr = someDependedFn('pascal-case-me')
  console.log(someNr)
}

packages/b/src/index.ts

import camelCase from 'camelcase'

export const someDependedFn = (str: string): string => {
  const camelStr = camelCase(str, { pascalCase: true })

  return camelStr
}

packages/b/src/typings/camelcase/index.d.ts

// Type definitions for camelcase 5.0
// Project: https://github.com/sindresorhus/camelcase

// tslint:disable only-arrow-functions completed-docs

declare module 'camelcase' {
  export default function camelCase(
    strs: string | string[],
    options: {
      pascalCase?: boolean
    }
  ): string
}

Now if you change directory to package a and run yarn build, it works just fine. But if you run yarn lint, it will throw:

$ tslint -p tsconfig.json

ERROR: packages/b/src/index.ts[4, 20]: Unsafe use of expression of type 'any'.
ERROR: packages/b/src/index.ts[6, 10]: Unsafe use of expression of type 'any'.

TSLint does not recognize the typings that are depended on by package B, but it only complains about this when running tslint from package A (not expected). Inside package B, tslint does not complain (as expected).

Question

Of course I could manually add the typings of camelcase inside package A, but that seems like an obvious violation of separation of concerns: package A is not supposed to know that package B depends on package camelcase, or X or Y. It is only supposed to know about package B's public API, i.e. the dependedFn.

How can I setup tslint such that it correctly resolves these indirect typing definitions when using yarn workspaces?

Epistrophe answered 19/7, 2018 at 19:36 Comment(0)
S
1

You can make TSLint work in your case by removing these lines from tsconfig.json:

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

These lines tell TypeScript compiler and TSLint that they should not treat your modules a and b as packages when you import them, but rather they should resolve individual TypeScript files using baseUrl and paths parameters and then compile individual TypeScript files. This behavior is documented in Module Resolution -> Path Mapping section of TypeScript documentation:

https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping

Instead, if I understood you right, you want to treat a and b as independent packages. To achieve this you should remove path mapping, and then TypeScript and TSLint will treat them as npm packages.

UPDATE (based on discussion in comments)

In your project you run TSLint using command:

tslint -p tsconfig.json but you run TSC using command:

tsc src/index.ts --outDir dist

Your TSLint uses TypeScript compiler API to do checks, based on rules from tsconfig.json. But your TypeScript compiler do not use tsconfig.json rules. In real-world projects both commands will use tsconfig.json

When you start using tsconfig.json for compilation too you will get the same problem with resolving 2nd degree dependencies types as you have with TSLint:

$ tsc -p tsconfig.json
../b/src/index.ts:1:23 - error TS7016: Could not find a declaration file for module 'camelcase'. '/home/victor/work/tslint-yarn-workspaces.org/node_modules/camelcase/index.js' implicitly has an 'any' type.
  Try `npm install @types/camelcase` if it exists or add a new declaration (.d.ts) file containing `declare module 'camelcase';`

1 import camelCase from 'camelcase'
                    ~~~~~~~~~~~

This happens because path-mapped module imports compiled differently by design, then normal imports from node_modules according to TypeScript documentation https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping as specified in the first part of the answer.

I would recommend using normal imports in your project to not have troubles with tools:

  1. Have "watch": "lerna run --parallel -- watch" script in workspace root package.json
  2. Have "watch": "tsc -p tsconfig.json -w" in workspace packages.
  3. Whenever you make changes to your project - start TypeScript compiler in watch mode in every package by running npm run watch in the workspace root.
Shantung answered 24/7, 2018 at 10:43 Comment(9)
Interesting, but then how would I actually get the benefit of my monorepo? I.e. how would packages resolve each other's paths?Epistrophe
They resolve paths through node_modules. Your package a has dependency on module b in package.json here: github.com/tommedema/tslint-yarn-workspaces/blob/… and because of that Yarn installs b into tslint-yarn-workspaces/package/node_modules as a symbolic link. After that tslint and typescript compiler can resolve b from there.Shantung
I see but that would break the ability to resolve type dependencies without compiling first. This is one of the primary reasons for using a different tsconfig for tslint and the editor.Epistrophe
I don't see this requirement in your question - that you don't want compile-first. And I don't see where are you using different tsconfig for tslint and the editor in your question. Please clarify. I am trying to answer your question.Shantung
I could add it to the question if you think that's better. It is already working like that in the example though. The repo I published is a simplified example of my actual setup, which is based on github.com/Quramy/lerna-yarn-workspaces-example -- you can see the different tsconfig for the editor vs compile time there.Epistrophe
Re: These lines tell TypeScript compiler and TSLint that they should not treat your modules a and b as packages when you import them, but rather they should resolve individual TypeScript files using baseUrl and paths parameters and then compile individual TypeScript files. this seems like the behavior that I do want. The question I guess is why this breaks the resolving of 2nd degree dependencies.Epistrophe
I would certainly do not recommend path mapping approach to connect packages with each another. With that approach you change resolution mechanism from standard to path-mapped which is different and many tools do not expect that you import packages in path-mapped way. So you will have all sort of troubles. Rather I'd recommend using normal flow, when you launch typescript compiler in watch mode inside each package via Lerna during development.Shantung
In my Yarn Workspaces TypeScript project github.com/sysgears/domain-schema I have started from this template as well github.com/Quramy/lerna-yarn-workspaces-example But after seeing all the issues with the tooling due to using non-standard package wiring via path mapping - I had to get rid of path mapping and the flow of development is very smooth nowShantung
Returning back to your comment: "The question I guess is why this breaks the resolving of 2nd degree dependencies". The answer is - in your project you are using tsconfig.json for your tslint and you are not using tsconfig.json for compilation. tslint under the hood uses compiler to check your source code, when you point it to tsconfig.json. If you start using tsconfig.json inside your npm run build script you will get errors about 2nd degree types too. And the reason you will get them I described in my answer - path-mapped modules compiled the different way by design.Shantung

© 2022 - 2024 — McMap. All rights reserved.