Reputation: 2879
I have a project that consists of multiple packages with a single common package. I have exactly the same declarations file in each package:
declare module '*.scss' {
const styles: { [className: string]: string };
export default styles;
}
What I want to achieve is to move this declarations to common package or additional common types package so TypeScript will recognize them automatically in every package that uses this common package.
Is there a way to achieve that?
Thank you in advance!
Upvotes: 2
Views: 1761
Reputation: 31873
The setup for this is actually rather straightforward. As we will see however, there are several options when it comes to consuming the shared declarations.
For exposition, we will give our shared package the highly inspired name of shared-package
and comprise it of the following 5 files:
package.json
{
"name": "shared-package",
"version": "1.0.0",
"main": "index",
"types": "index",
"devDependencies": {
"typescript": "^3.9.7"
},
"scripts": {
"tsc": "tsc",
"prepare": "tsc"
}
}
tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ESNext", // adjust as necessary depending on your target platform/toolchain
"module": "ESNext", // adjust as necessary depending on your target platform/toolchain
"moduleResolution": "Node",
"declaration": true,
"esModuleInterop": true
}
}
ambient-modules.ts
declare module '*.scss' {
const styles: {[className: string]: string};
export default styles;
}
utilities.ts
export const π = 3.142857142857143;
export interface Complex {
real: number;
imaginary: number;
}
index.ts
import './ambient-modules';
export * from './utilities';
For exposition we will create a simple package, blandly named consumer-package
, that depends on the shared-package
package we described above and is comprised of the following files
package.json
{
"name": "consumer-package",
"version": "1.0.0",
"main": "index",
"types": "index",
"dependencies": {
"shared-package": "^1.0.0"
},
"devDependencies": {
"typescript": "^3.9.7"
}
}
tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ESNext", // adjust as necessary depending on your target platform/toolchain
"module": "ESNext", // adjust as necessary depending on your target platform/toolchain
"moduleResolution": "Node",
"esModuleInterop": true
}
}
app.scss
.app {
text-transform: capitalize;
}
app.ts
import styles from './app.scss';
export default {
styles,
template: `
<div class="app">
hello world
</div>
`
};
index.ts
import app from './app';
export default function bootstrap() {
console.log('injecting styles: ', app.styles);
console.log('rendering markup: ', app.template);
}
However, we now encounter an error in our app.ts
file above,
Cannot find module './app.scss' or its corresponding type declarations.ts(2307)
, which is raised because, while we depend on our shared package, we have not written any code which depends on it.
We have fully established that consumer-package
depends on shared-package
, at the package level, but TypeScript does not yet see this dependency. The TypeScript language service is not doing an exhaustive crawl of our node_modules
directory looking for files containing ambient declarations that may be contained in miscellaneous dependencies.
(If it did, conflicts and unexpected errors would quickly arise).
There are several ways we can ensure that our ambient module declaration declare module '*.scss' {...}
is picked up by the language.
import anything from shared-package
in any source file in consumer-package
consumer-package/index.ts
import {Complex} 'shared-package';
import app from './app';
or import shared-package
for its side effects
consumer-package/index.ts
import 'shared-package';
import app from './app';
Note: this makes all types and values available, but may interfere with delivery optimizations like import elision and tree-shaking*
or use a triple slash reference directive to explicit state that you depend on declarations in shared-package
consumer-package/index.ts
/// <reference types="shared-package" />
import app from './app';
Note: this makes all types, without interfering with any potential optimizations, but makes your source code less portable and specifically more coupled to NodeJS and the behavior of specific package mangers*
or adjust your tsconfig.json
to explicitly list the types it depends on, including those in shared-package
.
consumer-package/tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"types": [
"shared-package"
]
}
}
Note: this makes all types, without interfering with any potential optimizations, but makes your source code less portable and specifically more coupled to NodeJS and the behavior of specific package mangers. However, it has a subjective advantage over the /// <reference>
approach in that it requires you to explicitly list the packages containing globally impactful, type level dependencies, such as declare module 'm'
constructs, that your own package itself needs
All of these approaches allow typed importing of .scss
files anywhere in consumer-package
, be informing TypeScript that consumer-package
depends on shared-package
in one way or another.
My preference is for either of the first 2 options.
In the "dependencies"
section of consumer-package/package.json
, the dependency on the package shared-package
is specified as "shared-package": "shared-package"
.
This assumes that it is in our package manager's registry which would be true if it were, say, published to https://npmjs.com.
If we are testing this out prior to publishing to our registry of choice, we can use the file system directly
Assuming, shared-package
and consumer-package
are in sister directories:
$ cd ./consumer-package
$ npm install ../shared-package
Which will result in the "dependencies"
section of consumer-package/package.json
listing it as "shared-package": "../shared-package"
. Other package managers, such as JSPM and Yarn, support the same functionality - consult their documentation for details.
Upvotes: 5