Eugene Tsakh
Eugene Tsakh

Reputation: 2879

How to create common declarations file for multiple packages?

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

Answers (1)

Aluan Haddad
Aluan Haddad

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.

Part 1: Creating the shared package

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';

Part 2: Consuming the shared package

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.

  1. import anything from shared-package in any source file in consumer-package

    consumer-package/index.ts

    import {Complex} 'shared-package';
    import app from './app';
    
  2. 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*

  3. 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*

  4. 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.

Notes:

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

Related Questions