Keywords: use types from TypeScript module without importing, publishing a package with types only, tell TypeScript to seek types in an NPM module.
I want to publish an NPM module that contains globally accessible types, much like lib.d.ts
.
What structure should the module have and how do I include it in another project?
If making the types globally visible is just too hard, requiring it with <reference/>
would be enough, but that didn't work when I tried.
In the project where I want to use the types, I've got a src
folder containing all the source code and bin
folder which contains the output of tsc
.
The module containing types can have virtually any structure, I don't really care as long as it works.
So far I've tried many, many combinations including export
ing the types, declare
ing the types, export declare
ing the types, putting them to .ts
or to .d.ts
file, moving them around the package's folder inside node_modules
, import
ing them, <reference/>
ing them, putting them to rootDirs
… But nothing worked. And the lack of good documentation on this also didn't help.
I had to solve this for my logging library, winston-jsonl-logger
. It augments the global scope with a global variable called logger
. I agree that this is one of the hardest (if not the hardest) problem in TypeScript, not least because of lack of sufficient documentation. In this example, I create a library that uses both globally-visible ('script') and module-visible ('module') types. To clarify that official terminology:
In TypeScript, just as in ECMAScript 2015, any file containing a top-level
import
orexport
is considered a module. Conversely, a file without any top-levelimport
orexport
declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).
My src
folder is transpiled into dist
. test
is ignored from transpilation.
It is imperative that you typings are named index.d.ts
and are nested in a folder whose name is the same as your project (which strictly is probably the name specified in package.json
). That's what structure typeRoots
will be looking for.
.
├── README.md
├── dist
│ ├── Logger.d.ts
│ ├── Logger.js
│ ├── Logger.js.map
│ ├── initLoggers.d.ts
│ ├── initLoggers.js
│ └── initLoggers.js.map
├── package-lock.json
├── package.json
├── src
│ ├── Logger.ts
│ └── initLoggers.ts
├── test
│ └── index.ts
├── tsconfig.json
└── typings
└── winston-jsonl-logger
└── index.d.ts
Script typings are those that lack a top-level import
or export
. They will be visible globally across projects that consume them.
Of course, as they can't use top-level import
declarations, they are limited in how descriptive they can be; you may often see a lot of any
used here. This is a problem I'm trying to get solved in my own question.
// typings/index.d.ts
declare namespace NodeJS {
export interface Global {
logger?: any;
log?: any;
logInfo?: any;
}
}
If you use logger
in the global scope, it will be typed as any
now.
Module typings can use top-level import
or export
, but they will only be seen if the module gets imported into the project. i.e. they are not visible globally across the project.
// initLoggers.ts
import {Logger} from "./Logger";
import {LogEntry, Logger as WinstonLogger} from "winston";
// Now we can be more descriptive about the global typings
declare global {
const logger: Logger;
// LogEntry's interface: { level: string, message: string, data?: any }
function log(entry: LogEntry): WinstonLogger;
function logInfo(message: string, data?: any): WinstonLogger;
}
export function initLoggers(){
global.logger = new Logger();
global.log = logger.log.bind(logger);
global.logInfo = (message: string, data?: any) => {
return logger.log({ level: "info", message, data });
}
}
If you use logger
in the global scope, it will still be typed as any
, but at least global.logger
will have proper typings.
To guarantee that these types are made visible across your project my-project
, make sure that my-project
imports this file from the winston-jsonl-logger
; I do it at my app's entrypoint.
package.json
I didn't use the typings
or types
field (maybe specifying "typings": "typings/winston-jsonl-logger/index.d.ts"
would have meant that packages don't have to explicitly declare the path to my typings; I don't know), but I did make sure to distribute my folder of typings.
{
"name": "winston-jsonl-logger",
"version": "0.5.3",
"description": "TypeScript JSONL logger.",
"main": "dist/Logger.js",
"files": [
"dist",
"typings"
],
"devDependencies": {
"@types/logform": "1.2.0",
"@types/node": ">=9.6.21",
"ts-node": "7.0.1",
"typescript": "3.1.1"
},
"dependencies": {
"winston": "3.2.0",
"winston-daily-rotate-file": "3.6.0",
"winston-elasticsearch": "0.7.4"
}
}
Omitted fields: repository
, keywords
, author
, license
, homepage
, publishConfig
, and scripts
; otherwise, that's everything.
tsconfig.json
Nothing special. Just your standard tsc --init
defaults.
Just make sure that you add a typeRoots
looks like this:
{
"compilerOptions": {
// ...All your current fields, but also:
"typeRoots": [
"node_modules/@types",
"node_modules/winston-jsonl-logger/typings/winston-jsonl-logger"
]
}
}
ts-node
There are further gotchas here. By default, ts-node
ignores script typings and only imports descendents of the entry-level import (the reason for this is speed/efficiency). You can force it to resolve imports just like tsc
does by setting the environment variable: TS_NODE_FILES=true
. Yes, it will run tests slower, but on the other hand, it'll work at all.
If you're using ts-node
via commandline, declare the TS_NODE_FILES
environment variable to be true
. I also had to declare TS_NODE_CACHE
to be false
, because of an inexplicable cache bug in ts-node
(version 7.0.1 – may still be an issue) when it's resolving imports/dependencies.
TS_NODE_FILES="true" TS_NODE_CACHE="false" TS_NODE_PROJECT="./tsconfigs/base.json" /usr/bin/nodejs --require ts-node/register --inspect=127.0.0.1:9231 src/index.ts --myCustomArg="hello"
I'm normally using ts-node
because I'm testing with Mocha. Here's how I pass environment variables to ts-node
from Mocha:
// mocha.env.js
/* From: https://github.com/mochajs/mocha/issues/185#issuecomment-321566188
* Via mocha.opts, add `--require mocha.env` in order to easily set up environment variables for tests.
*
* This can theoretically be made into a TypeScript file instead, but it seemed to not set the env variable when I tried;
* perhaps it failed to respect the order of the --require declarations. */
process.env.TS_NODE_FILES = "true"; // Force ts-node to use TypeScript module resolution in order to implictly crawl ambient d.ts files
process.env.TS_NODE_CACHE = "false"; // If anything ever goes wrong with module resolution, it's usually the cache; set to false for production, or upon any errors!
Hope this helps!
Similar to the other responders here, I have also spent a fair amount of time trying to get this right 😓. My use case is slightly different and I found an alternative way based on what I've seen other libs do.
Posting in case this is useful to anyone else.
My goal is slightly different than OP's: I wanted to publish global interfaces types and for the downstream users of my lib to have readily available whenever they are writing types, but I'm not trying to augment window
or global
, just publish global ambient types like React does (e.g. you don't have to import React
to use React.ComponentType
when writing types).
Here's how I did it:
ambient.d.ts
and I put it at the root of my project folder. This file is meant to be published alongside the dist
folder./index.d.ts
) that re-exports your compiled entry-point (./dist/index.d.ts
) and include in there a triple-slash reference (/// <reference types="./ambient" />
) to your ambient types file. Also ensure that your tsconfig.json
's include
option has ambient.d.ts
..d.ts
file at the root of their project folder containing a triple-slash reference to your lib. Many create-*-app
starters already create this file. For example, create-next-app
creates a next-env.d.ts
file that already contains triple-slash references. You could tell your users to augment that..
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── index.ts
│ └── initLoggers.ts
├── dist
│ ├── index.d.ts <-- your compiled index.d.ts file
│ ├── index.js
│ ├── index.js.map
│ ├── foo.d.ts
│ ├── foo.js
├── ambient.d.ts <-- write global types here
├── index.d.ts <-- new types entry point
├── tsconfig.json
You'd publish package.json
, ambient.d.ts
, index.d.ts
, and everything inside of dist
.
/package.json
:{
// ...
"types": "./index.d.ts", // specify the new entry types entry-point
// ...
}
/index.d.ts
and tsconfig.json
(step 2):// this is the new entry-point for your types
// use the triple-slash reference to bring in your ambient types
/// <reference types="./ambient" />
// re-export your compiled types
export * from './dist';
{
"compilerOptions": { /* ... */ },
"include": ["./src", "./ambient.d.ts"]
}
Tell them to create a blah.d.ts
file and add a triple-slash reference to your lib. As stated above, next.js already has this file and it's called next-env.d.ts
. You could tell your users to augment that or create a new *.d.ts
file.
/// <reference types="next" />
/// <reference types="next/types/global" />
// 👇👇👇
/// <reference types="your-published-lib-name" />
// 👆👆👆
👋 Alternatively, as other answers suggested, you could tell your users add your lib to the
typeRoots
compiler options intsconfig.json
but I prefer the triple-slash reference instead since it doesn't change the default compiler option and it's what I've seen other libs like next.js do.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With