Npm allows multiple types of dependencies, two of which are dependencies and peerDependencies. Semantically, they refer to different things: peerDependencies expresses the compatibility of a package with a host tool or library, usually referred to as a plugin, while dependencies expresses dependencies in the general sense.

Despite being two distinct types of dependencies, since v7, npm installs both types of dependencies by default. In this post, we explore the answer to the question: What are the differences between the effects of the two types of dependencies?

For the impatient, feel free to jump to the conclusion.

Basic Situation

In a basic situation, a package is specified in dependencies or peerDependencies and is not marked as optional, and there is no conflict of dependencies. The doc does not expressly state the differences in the effects of this situation, thus we ran a couple of small experiments with npm version 10.8.0.

The experiments included two packages, each containing a package.json file with a single dependencies or peerDependencies entry, respectively. We created a package.json file and put it in the dependencies/ directory:

{
  "name": "foo",
  "version": "1.0.0",
  "dependencies": {
    "react": "^18.3.1"
  }
}

Similarly, we replaced "dependencies" with "peerDependencies" and put it in the peerDependencies/ directory:

{
  "name": "foo",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "^18.3.1"
  }
}

npm install within the Package Directories

We ran npm install in each of the two directories. Then we ran diff -r dependencies/ peerDependencies/. Other than package.json, only package-lock.json and node_modules/.package-lock.json were different:

--- dependencies/package-lock.json	2024-05-19 15:48:24.581806109 -0700
+++ peerDependencies/package-lock.json	2024-05-19 15:48:28.189793011 -0700
@@ -7,19 +7,21 @@
     "": {
       "name": "foo",
       "version": "1.0.0",
-      "dependencies": {
+      "peerDependencies": {
         "react": "^18.3.1"
       }
     },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "peer": true
     },
     "node_modules/loose-envify": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
       "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "peer": true,
       "dependencies": {
         "js-tokens": "^3.0.0 || ^4.0.0"
       },
@@ -31,6 +33,7 @@
       "version": "18.3.1",
       "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
       "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "peer": true,
       "dependencies": {
         "loose-envify": "^1.1.0"
       },

The same applied to node_modules/.package-lock.json (omitted in the diff above).

This implies that:

  • The generated package-lock.json files only differed in indicating whether the dependency is peer or not.
  • All dependency package files installed in each directory were the same.

Installing the Two Packages

In this subsection, we installed the two packages above using npm. First, we packed the package by running npm pack in both the dependencies/ and peerDependencies/ directory, which generates a tarball named foo-1.0.0.tgz in each directory.

Then we created two directories named dependencies-user and peerDependencies-user, respectively. In each of the two directories, we ran npm install ../dependencies/foo-1.0.0.tgz and npm install ../peerDependencies/foo-1.0.0.tgz, respectively. Then we ran diff -r dependencies-user/ peerDependencies-user/:

diff --color -u --color -r dependencies-user/node_modules/foo/package.json peerDependencies-user/node_modules/foo/package.json
--- dependencies-user/node_modules/foo/package.json	2024-05-21 16:24:16.720145421 -0700
+++ peerDependencies-user/node_modules/foo/package.json	2024-05-21 16:24:10.848169061 -0700
@@ -1,7 +1,7 @@
 {
   "name": "foo",
   "version": "1.0.0",
-  "dependencies": {
+  "peerDependencies": {
     "react": "^18.3.1"
   }
 }
diff --color -u --color -r dependencies-user/package.json peerDependencies-user/package.json
--- dependencies-user/package.json	2024-05-21 16:01:04.069562379 -0700
+++ peerDependencies-user/package.json	2024-05-21 16:24:10.856169029 -0700
@@ -1,5 +1,5 @@
 {
   "dependencies": {
-    "foo": "file:../dependencies/foo-1.0.0.tgz"
+    "foo": "file:../peerDependencies/foo-1.0.0.tgz"
   }
 }
diff --color -u --color -r dependencies-user/package-lock.json peerDependencies-user/package-lock.json
--- dependencies-user/package-lock.json	2024-05-21 16:24:16.728145389 -0700
+++ peerDependencies-user/package-lock.json	2024-05-21 16:24:10.856169029 -0700
@@ -1,24 +1,26 @@
 {
-  "name": "dependencies-user",
+  "name": "peerDependencies-user",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "dependencies": {
-        "foo": "file:../dependencies/foo-1.0.0.tgz"
+        "foo": "file:../peerDependencies/foo-1.0.0.tgz"
       }
     },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "license": "MIT"
+      "license": "MIT",
+      "peer": true
     },
     "node_modules/loose-envify": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
       "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "js-tokens": "^3.0.0 || ^4.0.0"
       },
@@ -31,6 +33,7 @@
       "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
       "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "loose-envify": "^1.1.0"
       },
@@ -40,9 +43,9 @@
     },
     "node_modules/foo": {
       "version": "1.0.0",
-      "resolved": "file:../dependencies/foo-1.0.0.tgz",
-      "integrity": "sha512-asRRhQKMjcFJQLyC47nSqG9Rv6IsCsjnfvt4bxLs89KSWbOR+csGllJ1fIojRO4lFKweJFi79ZdvkFcigwhP+A==",
-      "dependencies": {
+      "resolved": "file:../peerDependencies/foo-1.0.0.tgz",
+      "integrity": "sha512-Rddf3EYuMq8GTrvn+oKIVX9C3MZzP8kVnFbXqxbkm0e346SJ1K6xJ7bO8OIy96tFI+tMX8vqWJ+a/l6pF8aPpw==",
+      "peerDependencies": {
         "react": "^18.3.1"
       }
     }

We observed that there was no difference other than the names of the dependencies and indications of whether the dependency was peer or not. (Diff in node_modules was the same as that in the previous subsection and is thus omitted.)

Install the Two Packages as Peer Dependencies

After emptying the dependencies-user/ and peerDependencies-user/ directories, we repeated the experiment in the previous subsection, but with npm install replaced with npm install --save-peer. The result was similar:

diff --color -u --color -r dependencies-user/node_modules/foo/package.json peerDependencies-user/node_modules/foo/package.json
--- dependencies-user/node_modules/foo/package.json	2024-05-21 16:30:57.282613821 -0700
+++ peerDependencies-user/node_modules/foo/package.json	2024-05-21 16:31:13.026556104 -0700
@@ -1,7 +1,7 @@
 {
   "name": "foo",
   "version": "1.0.0",
-  "dependencies": {
+  "peerDependencies": {
     "react": "^18.3.1"
   }
 }
diff --color -u --color -r dependencies-user/package.json peerDependencies-user/package.json
--- dependencies-user/package.json	2024-05-21 16:30:57.294613777 -0700
+++ peerDependencies-user/package.json	2024-05-21 16:31:13.038556061 -0700
@@ -1,5 +1,5 @@
 {
   "peerDependencies": {
-    "foo": "file:../dependencies/foo-1.0.0.tgz"
+    "foo": "file:../peerDependencies/foo-1.0.0.tgz"
   }
 }
diff --color -u --color -r dependencies-user/package-lock.json peerDependencies-user/package-lock.json
--- dependencies-user/package-lock.json	2024-05-21 16:30:57.294613777 -0700
+++ peerDependencies-user/package-lock.json	2024-05-21 16:31:13.042556045 -0700
@@ -1,11 +1,11 @@
 {
-  "name": "dependencies-user",
+  "name": "peerDependencies-user",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "peerDependencies": {
-        "foo": "file:../dependencies/foo-1.0.0.tgz"
+        "foo": "file:../peerDependencies/foo-1.0.0.tgz"
       }
     },
     "node_modules/js-tokens": {
@@ -43,10 +43,10 @@
     },
     "node_modules/foo": {
       "version": "1.0.0",
-      "resolved": "file:../dependencies/foo-1.0.0.tgz",
-      "integrity": "sha512-asRRhQKMjcFJQLyC47nSqG9Rv6IsCsjnfvt4bxLs89KSWbOR+csGllJ1fIojRO4lFKweJFi79ZdvkFcigwhP+A==",
+      "resolved": "file:../peerDependencies/foo-1.0.0.tgz",
+      "integrity": "sha512-Rddf3EYuMq8GTrvn+oKIVX9C3MZzP8kVnFbXqxbkm0e346SJ1K6xJ7bO8OIy96tFI+tMX8vqWJ+a/l6pF8aPpw==",
       "peer": true,
-      "dependencies": {
+      "peerDependencies": {
         "react": "^18.3.1"
       }
     }

(Diff in node_modules was the same as those in previous subsections and is thus omitted.)

Conflicting Dependencies

What if there are conflicts in dependencies?

Continuing the experimental setup in the previous section, we emptied the dependencies-user/ directory and added a package.json with the following content:

{
  "dependencies": {
    "foo": "file:../dependencies/foo-1.0.0.tgz",
    "react": "^17"
  }
}

Here, "react": "^17" is incompatible with the dependency of foo, which requires "react": "^18.3.1".

Similarly, we emptied the peerDependencies-user/ directory and added a package.json with the following content:

{
  "dependencies": {
    "foo": "file:../peerDependencies/foo-1.0.0.tgz",
    "react": "^17"
  }
}

Then, run npm install in dependencies-user/ and peerDependencies-user/, respectively. while npm install in dependencies-user/ succeeded, npm install in peerDependencies-user failed to resolve dependencies:

npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: undefined@undefined
npm error Found: react@17.0.2
npm error node_modules/react
npm error   react@"^17" from the root project
npm error
npm error Could not resolve dependency:
npm error peer react@"^18.3.1" from foo@1.0.0
npm error node_modules/foo
npm error   foo@"file:../peerDependencies/foo-1.0.0.tgz" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.

This seems to be what this quoted part of npm doc on peerDependencies refers to:

Trying to install another plugin with a conflicting requirement may cause an error if the tree cannot be resolved correctly.

Further inspection into dependencies-user/ showed that a copy of react version 18 lived in node_modules/foo/node_modules/react, and a copy of react version 17 lived in node_modules/react. In other words, two versions of react existed in dependencies-user/, which might cause runtime errors if foo was meant to be a plugin to an existing react package (such as a React component) instead of bringing its own dependency of react.

Optional Dependencies

What if a dependency is specified as optional? The doc of npm has given a clear answer.

Optional Dependencies in optionalDependencies

An optional dependency can be specified via optionalDependencies. The doc of optionalDependencies reads:

Running npm install --omit=optional will prevent these dependencies from being installed.

In other words, unless explicitly omitted, npm installs optional dependencies specified in optionalDependencies by default.

Optional Peer Dependencies

An optional peer dependency can be specified via peerDependenciesMeta. The doc of peerDependenciesMeta reads:

Npm will not automatically install optional peer dependencies. This allows you to integrate and interact with a variety of host packages without requiring all of them to be installed.

Summary

By default, npm installs optional dependencies specified in optionalDependencies, but not optional peer dependencies.

Conclusion

  • When there is no optional dependency or conflict of dependencies, our experiments showed that there is no difference in the effects of specifying a package in dependencies or peerDependencies, other than indications of whether they are peer dependencies or not. (The differences in their semantics for human readers still hold.)
  • A conflict of dependencies can occur when package bar depends on foo, and package baz depends on bar and an incompatible version of foo. When there is such kind of conflict of dependencies, our experiments showed that npm reports an error if foo is a peer dependency of bar, but installs two versions of foo if the dependency is specified in dependencies.
  • When there are optional dependencies, by default, npm installs packages included in optionalDependencies, but not those specified in peerDependenciesMeta as optional peer dependencies.