Search code examples
typescriptnpmmonorepo

How to create a types only package on NPM


I would like to create a package (monorepo or NPM) that export only types so that in my project I can import them as such

import type { MyType } from '@acme/types'

I have tried the following solution but it doesn't work

Project structure

package.json
index.d.ts

index.d.ts

export type MyType = {
  name: string
}

package.json

{
  "name": "@acme/types",
  "main": "./index.d.ts",
  "types": "./index.d.ts",
  "exports": {
    ".": {
      "default": "./index.d.ts",
      "import": "./index.d.ts",
      "types": "./index.d.ts"
    },
    "./package.json": "./package.json"
  }
}

When I want to import my type, I get this error

File '/Users/acme/packages/types/index.d.ts' is not a module. ts(2306)

I have also tried to rename index.d.ts to index.ts. Doesn't work either


Solution

  • See Declaration Files > Publishing in the TypeScript handbook for information related to this topic, and check out the DefinitelyTyped repository for lots of examples.


    In a simple scenario like the one described by the details in your question, the only parts needed in the provider package are:

    1. the type declaration files, including an entrypoint declaration file named index.d.ts, and

    2. the minimum criteria for meeting the definition of a Node.js module: a package.json manifest file with the required fields: name and version

    So, given that your provider package directory is located at ./provider, the required files would look like this:

    ./provider/package.json:

    {
      "name": "@acme/types",
      "version": "0.1.0"
    }
    
    

    ./provider/index.d.ts:

    export type MyType = {
      name: string;
    };
    
    

    The following note is included in the linked TS handbook page, in the section Including declarations in your npm package:

    Also note that if your main declaration file is named index.d.ts and lives at the root of the package you do not need to mark the types property, though it is advisable to do so.

    So, you might consider defining that field explicitly rather than relying on automatic resolution behavior:

    ./provider/package.json:

    {
      "name": "@acme/types",
      "version": "0.1.0",
      "types": "./index.d.ts"
    }
    
    

    Now, that's the answer to the question asked — but I'll also include an example consumer package with reproduction steps for demonstration.

    If you want to follow along, you can copy + paste each of the following example files, recreating them in your filesystem. Or you can just run this script in your browser's JS console to download a zip archive of the entire project structure:

    (() => {
      function createBase64DataUrl(mediaType, b64Str) {
        return `data:${mediaType};base64,${b64Str}`;
      }
    
      function download(url, fileName) {
        const a = document.createElement("a");
        a.href = url;
        a.download = fileName;
        a.click();
        a.remove();
      }
    
      const zipArchiveData = "UEsDBAoAAAAAADQOe1YAAAAAAAAAAAAAAAAJABwAcHJvdmlkZXIvVVQJAAODPCFkjjwhZHV4CwABBPUBAAAEFAAAAFBLAwQUAAAACABObHtWF0wQhEEAAABNAAAAFQAcAHByb3ZpZGVyL3BhY2thZ2UuanNvblVUCQADs+EhZLPhIWR1eAsAAQT1AQAABBQAAACr5lJQUMpLzE1VslJQckhMzk3VL6ksSC1W0gFJlKUWFWfm54HkDPQM9QwgohAFQDE9/cy8lNQKvRS9kmIlrlouAFBLAwQUAAAACAA5DntWXPRvjCkAAAAqAAAAEwAcAHByb3ZpZGVyL2luZGV4LmQudHNVVAkAA448IWSPPCFkdXgLAAEE9QEAAAQUAAAAS60oyC8qUSipLEhV8K0MAVG2CtVcCgp5ibmpVgrFJUWZeenWXLXWXABQSwMECgAAAAAA53R7VgAAAAAAAAAAAAAAAAkAHABjb25zdW1lci9VVAkAA+HwIWTk8CFkdXgLAAEE9QEAAAQUAAAAUEsDBBQAAAAIANdye1bERSpvhQAAAL8AAAAVABwAY29uc3VtZXIvcGFja2FnZS5qc29uVVQJAAMF7SFkBe0hZHV4CwABBPUBAAAEFAAAAE2NvQrDMAwGdz+F0VwUt9AlU4c+RyHIKhjqH2zXNIS8e2WnQ9fvTqdNaQ1h8QyzBoqhvD1nOPW1cS4uhg4MntEcq+V258TBciDHRegms4DbQp6nuqYxwtO9eEacUo7N2V9TtCFQdql263FFgxcQtI/6Qf6qFH2SUndrIe3k7wdF6BdqV19QSwMEFAAAAAgAcXB7VscThVtUAAAAZAAAABEAHABjb25zdW1lci9pbmRleC50c1VUCQADhughZIfoIWR1eAsAAQT1AQAABBQAAADLzC3ILypRKKksSFWoVvCtDAExahXSivJzFZQcEpNzU/VBcsVK1lxcyfl5xSUK+UlZVjCFtkA9eYm5qVYKSmn5+UoKtVBV+Tmpejn56RpAtZrWXABQSwECHgMKAAAAAAA0DntWAAAAAAAAAAAAAAAACQAYAAAAAAAAABAA7UEAAAAAcHJvdmlkZXIvVVQFAAODPCFkdXgLAAEE9QEAAAQUAAAAUEsBAh4DFAAAAAgATmx7VhdMEIRBAAAATQAAABUAGAAAAAAAAQAAAKSBQwAAAHByb3ZpZGVyL3BhY2thZ2UuanNvblVUBQADs+EhZHV4CwABBPUBAAAEFAAAAFBLAQIeAxQAAAAIADkOe1Zc9G+MKQAAACoAAAATABgAAAAAAAEAAACkgdMAAABwcm92aWRlci9pbmRleC5kLnRzVVQFAAOOPCFkdXgLAAEE9QEAAAQUAAAAUEsBAh4DCgAAAAAA53R7VgAAAAAAAAAAAAAAAAkAGAAAAAAAAAAQAO1BSQEAAGNvbnN1bWVyL1VUBQAD4fAhZHV4CwABBPUBAAAEFAAAAFBLAQIeAxQAAAAIANdye1bERSpvhQAAAL8AAAAVABgAAAAAAAEAAACkgYwBAABjb25zdW1lci9wYWNrYWdlLmpzb25VVAUAAwXtIWR1eAsAAQT1AQAABBQAAABQSwECHgMUAAAACABxcHtWxxOFW1QAAABkAAAAEQAYAAAAAAABAAAApIFgAgAAY29uc3VtZXIvaW5kZXgudHNVVAUAA4boIWR1eAsAAQT1AQAABBQAAABQSwUGAAAAAAYABgAEAgAA/wIAAAAA";
      const dataUrl = createBase64DataUrl("application/zip", zipArchiveData);
      download(dataUrl, "so-75850348.zip");
    })();
    

    Given that the consumer package directory is located at ./consumer, it might start with this minimal package file:

    ./consumer/package.json:

    {
      "name": "consumer",
      "version": "0.1.0"
    }
    
    

    The first step is to install TypeScript and the provider types package as development dependencies:

    $ cd ./consumer
    $ npm install --save-dev typescript ../provider
    
    added 2 packages, and audited 4 packages in 616ms
    
    found 0 vulnerabilities
    

    Each package could have been installed using separate commands:

    npm install --save-dev typescript
    npm install --save-dev ../provider
    

    Afterward, the package.json will include these dependencies:

    {
      "name": "consumer",
      "version": "0.1.0",
      "devDependencies": {
        "@acme/types": "file:../provider",
        "typescript": "^5.0.2"
      }
    }
    
    

    We want to use the types in some code, so let's create a basic TypeScript file which will use the types from the package:

    ./consumer/index.ts:

    import type { MyType } from "@acme/types";
    
    const obj: MyType = { name: "foo" };
    
    console.log(obj);
    
    

    Next, let's add an npm script to the package.json file which will compile the TypeScript file using the compiler defaults — we'll name the script compile:

    {
      "name": "consumer",
      "version": "0.1.0",
      "devDependencies": {
        "@acme/types": "file:../provider",
        "typescript": "^5.0.2"
      },
      "scripts": {
        "compile": "tsc index.ts"
      }
    }
    
    

    In most scenarios you'll probably want to configure the compiler behavior for your project using a TSConfig.

    Now, let's compile the file and run it:

    $ npm run compile && node index.js
    
    > [email protected] compile
    > tsc index.ts
    
    { name: 'foo' }
    

    Compilation succeeds without issue and the file runs as expected.

    Note that I'm using this Node LTS version as I write this answer:

    $ node --version
    v18.15.0