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

Exploring through objects extensively and expanding all their values within Angular

I am in need of searching for a specific value within an object graph. Once this value is found, I want to set the 'expanded' property to true on that particular object, as well as on all containing objects up the object graph. For example, give ...

Accessing data retrieved from an API Subscribe method in Angular from an external source

Below is the Angular code block I have written: demandCurveInfo = []; ngOnInit() { this.zone.runOutsideAngular(() => { Promise.all([ import('@amcharts/amcharts4/core'), import('@amcharts/amcharts4/charts') ...

Employ various iterations of the leaflet library

Creating a web application using React and various libraries, I encountered an issue with conflicting versions of the Leaflet library. Currently, I am incorporating the Windy API for weather forecast, which utilizes Leaflet library version 1.4.0. However ...

Expose sub-modules within npm packages for added functionality

I have a private library that I want to share between two code bases. The structure is as follows: package.json src/ |_stores/ |_user.js |_actions/ |_user.js The project is named "foo" and I am looking for the best way to import it like this: ...

"Is it possible in Typescript to set the parameters of a returning function as required or optional depending on the parameters of the current

I am currently exploring Typescript and attempting to replicate the functionality of styled-components on a smaller scale. Specifically, I want to make children required if the user passes in 'true' for the children parameter in createStyledCompo ...

Guide on merging paths in distinct modules within an Angular project

I am looking to merge two sets of routes from different modules in my application. The first module is located at: ./app-routing.module.ts import {NgModule} from '@angular/core'; import {Routes, RouterModule} from '@angular/router'; i ...

Using the HTTP Post method to retrieve a file object: a step-by-step guide

Is there a way to utilize a http POST request in order to retrieve a file object? Though the uploading of files to the server using the POST request seems successful and flawless, attempting to fetch the file results in an unusual response: console output ...

Creating a Versatile Data Retrieval Hook in TypeScript for APIs with Diverse Response Formats

Currently, I am undertaking a movie discovery project that involves fetching data from two APIs: Tmdb movie site. These APIs have different response structures—one for movies and one for genres. In order to streamline my code and avoid repeating the same ...

A guide to sending epoch time data to a backend API using the owl-date-module in Angular

I have integrated the owl-date-time module into my application to collect date-time parameters in two separate fields. However, I am encountering an issue where the value is being returned in a list format with an extra null value in the payload. Additiona ...

The combination of TypeScript decorators and Reflect metadata is a powerful tool for

Utilizing the property decorator Field, which adds its key to a fields Reflect metadata property: export function Field(): PropertyDecorator { return (target, key) => { const fields = Reflect.getMetadata('fields', target) || []; ...

Error: The "res.json" method is not defined in CustomerComponent

FetchData(){ this.http.get("http://localhost:3000/Customers") .subscribe(data=>this.OnSuccess(data),data=>this.OnError(data)); } OnError(data:any){ console.debug(data.json()); } OnSuccess(data:any){ this.FetchData(); } SuccessGe ...

Oops! There seems to be an issue with the code: "TypeError: this

I am just starting out with Angular. Currently, I need to assign a method to my paginator.getRangeLabel (I want to use either a standard label or a suffixed one depending on certain conditions): this.paginator._intl.getRangeLabel = this.getLabel; The cod ...

SonarLint is suggesting that the parameter currently in use should either be removed or renamed

According to Sonar lint: Suggestion: Remove the unused function parameter "customParams" or rename it to "_customParams" for clearer intention. However, the parameter "customParams" is actually being used in the method "getNextUrl". What am I doing wron ...

Using interfaces for typecasting in TypeScript

Consider the code snippet below: let x = { name: 'John', age: 30 } interface Employee { name:string, } let y:Employee = <Employee>x; console.log(y); //y still have the age property, why What is the reason for TypeScript ov ...

Tips for fixing the HTTP error 431 in Next.js with Next-Auth

I am encountering an issue with rendering a photo in jwt via token. Tools utilized: nextjs, typescript, next-auth, keycloak, LDAP The image is retrieved from LDAP and passed to the keycloak user. My application is responsible for storing the jwt token po ...

Refreshing Angular 9 component elements when data is updated

Currently, I am working with Angular 9 and facing an issue where the data of a menu item does not dynamically change when a user logs in. The problem arises because the menu loads along with the home page initially, causing the changes in data to not be re ...

Creating unique components with Angular2 and Ionic

Here is the HTML code for my custom component: <div> {{text}} {{percentLeft}} {{innerColor}} </div> And here is the TypeScript file for my component: import { Component, Input } from '@angular/core'; @Component({ selector: ...

NPM ERROR: npm ERR! failed to identify the executable for execution

Encountered an error while running my application: npm ERR! could not determine executable to run Error log: cat /home/harry/.npm/_logs/2021-09-09T11_15_34_872Z-debug.log 0 verbose cli [ 0 verbose cli '/usr/local/bin/node', 0 verbose cli &ap ...

The replacer argument of the JSON.stringify method doesn't seem to work properly when dealing with nested objects

My dilemma is sending a simplified version of an object to the server. { "fullName": "Don Corleone", "actor": { "actorId": 2, "name": "Marlon", "surname": "Brando", "description": "Marlon Brando is widely considered the greatest movie actor of a ...

When it comes to semantic versioning, which release number gets incremented when a module introduces compatibility improvements without causing any disruptions?

Recently, I encountered an npm module that was not working well with React 18 and Next.js. So, I took it upon myself to tweak certain aspects of the module to make it compatible with these packages. Thankfully, my modifications did not disrupt the experien ...