#`typeRoots` and `types` or: how do I add types for a package without `node_modules/@types`?

1 messages · Page 1 of 1 (latest)

grizzled raft
#

Hi,
I am maintaining a package @y/impl-package-types, which contains the type definitions for package @x/impl-package (note that they live under different scopes. I am not sure if this has any relevance, I just added it for completeness sake). I can not move @y/impl-package-types to Definitely Typed, nor can I add and ship the types with @x/impl-package.
Assume @y/impl-package-types consists of just one index.d.ts on root level with the following contents:

// index.d.ts
declare module '@x/impl-package' {
  export function fn (x: number, y: number): number
}

which is the full description of @x/impl-package's API. I want to make these types available when the user does:

import * as impl from '@x/impl-package'

One way of doing this seems to be adding either compilerOptions.types: ["@y/impl-package-types"] or compilerOptions.typeRoots: ["./@y"] to the consumer's tsconfig.json. In both cases, the types can be properly resolved. But I am confused by the description for both options in the manual. Based on the manual, specifying these options should override their default behaviour.

For example, the docs for typeRoots says:

If typeRoots is specified, only packages under typeRoots will be included.
https://www.typescriptlang.org/tsconfig/#typeRoots

So I would assume types living in ./node_modules/@types/… (the default value) to no longer be found. But I can not verify this. A consumer project with the following files resolves the types for both my impl project and express perfectly fine:

#
// index.ts
import * as impl from '@x/impl-package'
import { Router } from 'express'

console.log(impl.fn(40, 2))
console.log(Router)
// package.json
{
  "name": "consumer",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "express": "^5.1.0",
    "@x/impl-package": "file:../impl-package"
  },
  "devDependencies": {
    "@types/express": "^5.0.3",
    "@y/impl-package-types": "file:../impl-package-types"
  }
}
{
  "compilerOptions": {
    "module": "nodenext",
    "target": "esnext",
    "types": ["impl-package-types"],  // weirdly, no scope required here
    "strict": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
  }
}

I would have assumed that the LSP can no longer find the types in ./node_modules/@types/express, but that does not seem to be the case.
Am I misunderstanding these config options?

stray otter
#

@grizzled raft I'm a little confused as you're asking about typeRoots but your example shows types. But yeah, this looks correct. What types does is control which globally modifying typings are automatically loaded from node_modules/@types

grizzled raft
#

I am asking about both. I can construct the the tsconfig.json the same using typeRoots with the same outcome of the default seemingly still being in effect

stray otter
#

There's basically two sorts of typings:

  1. Typings that describe a package of the same name - when you import that package name, TS looks in [typeRoot]/packageName (i.e. node_modules/@types/express by default
  2. Typings that are 'ambiently' loaded - stuff like @types/node - you don't import node from "node", the typings are (by default) implicitly loaded and define things like a "fs" module.
#

types affects the second sort - if you specify types it will only load the packages you list in that list. So types: [] would prevent node typings from being available in a project, even if @types/node is installed. (I recommend specifying the types config) But it doesn't stop importing things like express because that uses mechanism #1

grizzled raft
#

okay, so if I understand you correctly, by having specified an explicit value for compilerOptions.types, all other global types should be gone. But that does also not seem to be the case for node:fs

stray otter
#

Well, unless something that you're importing explicitly includes the node types, which is possible too. I don't know if either express or your impl-package-types does that.

grizzled raft
#

good call! By removing the import from express the types for node:fs are now gone

stray otter
#

Yeah, I guess not surprising express has some fairly tight coupling with node APIs and probably needs those types loaded.

grizzled raft
#

okay, so global types are (unsurprisingly, actually) transitive. That clears things up a little. I assume there is no way of, instead of overriding the default, appending to it?

stray otter
#

The default for what?

grizzled raft
#

The default for compilerOptions.types

stray otter
#

The default for types is to include everything it finds.

#

So 'appending' doesn't really make sense.

grizzled raft
#

Where does it look?

#

okay, I guess in node_modules/@types et al.
I was hoping to be able to add more folders beside …/@types; specifically, node_modules/@y to include @y/impl-package-types. But I guess for that I have to actually use typeRoots. Then that shifts my question: how to append another directory to compilerOptions.typeRoots, instead of overriding its default?

stray otter
#

You can list ./node_modules/@types as a type-root and that should work

#

But also it looked like this was working without that?

grizzled raft
#

will that include ../node_modules/@types, ../../node_modules/@types, etc., which is part of the default?

stray otter
#

Not sure. It mostly only affects ambiently typed modules (#2) not explicitly imported ones (#1) so in most cases I don't think it would matter.

#

But it does seem like it'd be easier to let impl-package-types be treated as an 'ambient' types package (#2 above) and install it in node_modules/@types(and list it in impl-package-types if necessary).

grizzled raft
#

how do I install it into node_modules/@types without moving in with Definitely Typed?

stray otter
#

Ah fair point - I guess the cases I'm thinking of (e.g. @types/webpack-env) are still registered in DefinitelyTyped.

#

i guess another option would be to structure your package as if it were the normal typings for impl-package and then use a path mapping.

grizzled raft
#

Definitely Typed
yes, that would make everything a lot easier. Sadly, it is not an option for me. So I am currently working around it in ways that involve sacrificing a virgin. The last thing I did was add a postInstall script that would symlink @y/impl-package-types@types/y__impl-package-types. But that would break all the time, so I am now looking for alternatives.

stray otter
#
"paths": {
    "@x/impl-package": "./node_modules/my-impl-package" // path is relative to baseUrl if there is one
}
grizzled raft
#

where does that go? In the consuming package?

stray otter
#

Yeah.

grizzled raft
#

alright, that does look promising!
I'll toy around with it some more tomorrow, as I should have clocked out an hour ago.
Thanks for all the help and patience so far

stray otter
#

to structure your package as if it were the normal typings for
What I mean by this is that non-ambient types don't include declare module 'package-name' { } they just have a .d.ts file that exports the public contract.

grizzled raft
#

I'd like to keep this thread open until I had the chance to investigate a bit more to maybe follow up (not expecting you personally to respond, but users in general)

stray otter
#

Maybe the declare module 'package-name' {} pattern works too, though.

grizzled raft
#

I'd have to look into that too