What is the best way to manage optional peer dependency types while releasing a TypeScript package?

I'm trying to figure out the best way to handle optional peer dependencies when publishing a TypeScript package on npm. My package provides a function that can accept input from either one of two peer dependencies. How should I define these optional peer dependencies?

import { ExternalFoo } from 'foo';
import { ExternalBar } from 'bar';

export const customPackage = (source: ExternalFoo | ExternalBar) => {
    /* ... */
}

Is there a way to ensure that users of my package won't encounter errors if they have only one of the required dependencies installed, but not both?

Answer №1

With the introduction of Typescript 3.8, a new syntax is available:

import type { ExternalFoo } from "foo";

If you are simply using the library for type information, there may no longer be a need to list it as a dependency or an optionalDependency. Instead, you could consider specifying it as a peerDependency, ensuring that users have compatible versions with your library. Including it as a devDependency can also be beneficial.

It's important to note that this import will only appear in the generated d.ts files and not in the transpiled .js code. However, if the library is not installed by users, the type will default to any, potentially affecting your own typing. This could lead to issues such as:

customPackage = (source: any | ExternalBar) =>
// equivalent to customPackage = (source: any) =>

In scenarios where the library is missing, the type annotation won't properly utilize related types even if they are present. While there is a method to reduce dependency on external libraries, challenges still exist in maintaining type annotations that remain robust regardless of their presence.

To explore solutions for handling missing types, refer to this answer.

For more details on Type-Only Imports and Exports in Typescript 3.8, visit the reference page.

Answer №2

After exploring various solutions, I have discovered a robust approach that works well with the latest versions of TypeScript (as of late 2021):

// @ts-ignore -- defining an optional interface to gracefully handle scenarios where `foo` is not available
import type { Foo } from "foo";
import type { Bar } from "bar";

// Determines the argument type based on the availability of `foo`
type Argument = any extends Foo ? Bar : (Foo | Bar);

export function customPackage(source: Argument): void {
  ...
}

You can experiment with this solution yourself. If the foo module is present, the method will accept arguments of type Foo or Bar, and if it is not available, it will only allow Bar (not any).

Answer №3

Unfortunately, TypeScript currently struggles to fully support your specific scenario.

To summarize your situation:

  1. Your dependency on foo and bar is optional, assuming that your consumers will use one of them with your library.
  2. You are solely utilizing the type information from these libraries without any code dependencies, and you prefer not to add them as dependencies in your package.json.
  3. Your customPackage function is public.

However, due to point 3, you must include these types in your library typings, which contradicts points 1 and 2 since it requires adding foo and bar as dependencies.

If the typings for foo and bar come from DefinitelyTyped (e.g., package @types/foo and @types/bar), adding them as dependencies in your package.json should resolve the issue.

Alternatively, if the typings are distributed with the libraries themselves, you can either add the libraries as dependencies (against your preference) or generate replicas of the types such as ExternalFoo and ExternalBar.

This approach would disconnect your reliance on foo and bar.

Another option is to reevaluate your library and consider the implications of including foo and bar as dependencies. Depending on your library's nature, this may not be as detrimental as anticipated.

Personally, I tend to opt for declaring the types independently, given JavaScript's dynamic nature.

Answer №4

Dealing with a complex scenario, I have discovered a solution that involves inserting a ts-ignore prior to importing the type that may not be present in the user's system:

// @ts-ignore
import type { Something } from "optional"
import type { AnotherThing } from "necessary"

This allows you to include the package in both peerDependencies and peerDependenciesMeta as optional.

Answer №5

Context

An issue arises when utilizing optional dependencies within a file that is accessible through the primary entrypoint of your package (e.g., ./index.ts).

Resolution

To address this problem, consider breaking down your code to have multiple entry points. For instance, maintain one primary entry point and include two optional modules – FooModule and BarModule – which users must import explicitly.

Snippet

This section illustrates the package definition and how the library is utilized (imports).

library's package.json

The package.json specifies several entry points (exports) along with their corresponding type definitions (typesVersions).

{
  "name": "my-package"
  "version": "1.0.0",
  "main": "./dist/index.js",
  "exports": {
    ".": "./dist/index.js",
    "./foo": "./dist/modules/foo/index.js",
    "./bar": "./dist/modules/bar/index.js"
  },
  "typesVersions"<span;>{
    "*": {
      "*": [
        "dist/index.d.ts"
      ],
      "foo": [
        "dist/modules/foo/index.d.ts"
      ],
      "bar": [
        "dist/modules/bar/index.d.ts"
      ]
    }
  },
}
how my-package is used in client code

By solely importing the FooModule, there is no requirement to install optional dependencies for the BarModule.

import { MyPackage } from 'my-package';
import { FooModule } from 'my-package/foo';

const myPackage = new MyPackage({ module: new FooModule() })

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to combine properties from Type1 and Type2 to create a new type in Typescript?

Here is a snippet of code I'm working with: interface Notification { message: TemplatedEmail & Email, //current attempt which doesnt do what I want } interface Destination { ccAddresses?: string[], bccAddresses?: string[], toAddresses: st ...

Can two different versions of a library be utilized simultaneously in NPM?

Currently, our Vue.js app is built with Vuetify v1.5 and we are considering transitioning to Vuetify 2.0. However, the process would involve numerous breaking changes which we currently do not have the resources to address for all components. Is there a wa ...

What is the importance of adding the ".js" extension when importing a custom module in Typescript?

This is a basic test involving async/await, where I have created a module with a simple class to handle delays mymodule.ts: export class foo { public async delay(t: number) { console.log("returning promise"); ...

Unable to use global modules in NestJS without importing them

Currently, I am in the process of integrating a global module into my nest.js project I have written a service as shown below: export interface ConfigData { DB_NAME: string; } @Injectable() export class ConfigManager { private static _inst ...

Is there a way to prevent npm from bootstrapping when pushing to Heroku using git?

I've been developing a Facebook Messenger bot and hosting it on Heroku. In the package.json file, I have specified my node and NPM versions like this: "engines": { "node": "8.10.0", "npm": "5.7.1" }, Whenever I push changes to Heroku using ...

All authentication logic in Angular encapsulated within the service

I am considering moving all the business logic into the auth service and simply calling the method on the component side. Since none of my functions return anything, I wonder if it's okay or if they will hang. COMPONENT credentials: Credentials = ...

Encountering difficulties while setting up ESLint with Sublime Text

I have recently started exploring Node.js, npm, and the sublime environment, but I am facing challenges when trying to integrate ESLint with my sublime editor. Here are the steps I followed: Installed ESLint using the command: npm install -g eslint Inst ...

Error in TypeScript while running the command "tsd install jquery" - the identifier "document" could not be found

Currently, I am facing an issue with importing jQuery into my TypeScript project. In order to achieve this, I executed the command tsd install jquery --save, which generated a jquery.d.ts file and added /// <reference path="jquery/jquery.d.ts" /> to ...

Accessing the state from a child functional component and then adding it to an array of objects in the parent component

I'm facing a challenge with a parent component that needs to manage the data of its child components stored in an array of objects. My goal is to add new child components and maintain their information within the parent's state as an array of obj ...

Executing npm / http-server script

I'm currently working on a shell script that will compile an Angular app using the "ng build" command and then launch a web server to host the app from the dist folder. The web server will be started with the "http-server" command, which needs to be i ...

Having trouble retrieving an object property in HTML or TypeScript within an Angular framework?

export class ComponentOne { array_all_items: Array<{ page_details: any }> = []; array_page_details: Array<{ identifier: number, title: string }> = []; initial_item: Array<{ identifier: number, title: string }> = [ { ...

I am unable to locate the module 'fs': I have exhausted all possible solutions to fix this problem

Attempting to delete a file from the local system using the unlink function, but encountering an error stating that it cannot find the module 'fs'. Below are some details on the relevant files: app.component.ts import * as fs from 'fs&apos ...

The color scheme detection feature for matching media is malfunctioning on Safari

As I strive to incorporate a Dark Mode feature based on the user's system preferences, I utilize the @media query prefers-color-scheme: dark. While this approach is effective, I also find it necessary to conduct additional checks using JavaScript. de ...

I attempted to use ng add @angular/pwa, but encountered an error code that is baffling to me. Are there any steps I can take to resolve this issue?

npm ERR! ERESOLVE could not resolve npm ERR! npm ERR! While trying to find a solution: [email protected] npm ERR! Found: @angular/[email protected] npm ERR! in node_modules/@angular/common npm ERR! @angular/common@"^14.2.3" ...

Error: Attempting to access a property named '_updatedFibers' on an undefined object is not possible due to a TypeError

I encountered the following error: Uncaught TypeError: Cannot read properties of undefined (reading '_updatedFibers') at requestUpdateLane (react-dom.development.js:25411:23) at updateContainer (react-dom.development.js:28810:14) at ReactDOMHydra ...

What is the reason for not opening the ndb debugger in the package.json file?

Here is my package.json file: { ... "scripts": { "start": "nodemon server.js", "start:prod": "NODE_ENV=production nodemon server.js", "debug": "ndb server.js" }, ... The npm command ndb has been made global. However, when I run npm run d ...

Encountering vulnerabilities during NPM installation, attempting to fix with 'npm audit fix' but unsuccessful

While working on my react project, I decided to incorporate react-icons by running npm install react-icons in the command prompt. However, after some time, the process resulted in the following errors: F:\Areebs\React JS\areeburrub>npm in ...

Travis CI's TypeScript build process detects errors before Mocha has a chance to catch them

Instead of a bug, the TypeScript compiler is doing its job but causing my Travis builds to fail. In my package, I have a function named completeRound which accepts a number as its first argument and 3 optional arguments. Since it's in TypeScript, I s ...

Angular strictPropertyInitialization - best practices for initializing class members?

When initializing a component, I need to retrieve user information. However, with the Angular strict mode in place, I'm uncertain about where to fetch this data. I have considered 3 options. But which one is the most appropriate? Is there another alt ...

Typescript validation of tokens using Azure functions

Currently working on a website utilizing Azure Static Web App, where the login/registration is managed by Azure B2C. The backend API consists of typescript Azure functions integrated with Azure Static web app. Certain API calls can only be accessed when th ...