@httpx/plain-object
v2.4.0
Published
Fast and lightweight utility functions to check if a value is a plain object.
Maintainers
Readme
@httpx/plain-object
Fast and lightweight (~80B) functions to check or assert that a value is a plain object.
A plain object is a basic JavaScript object, such as {}, { data: [] }, new Object() or Object.create(null).
See how it compares to other libraries.
Install
$ npm install @httpx/plain-object
$ yarn add @httpx/plain-object
$ pnpm add @httpx/plain-objectFeatures
- 👉 Provide isPlainObject and assertPlainObject functions.
- 🦄 Convenience PlainObject typescript typings.
- 🚀 Faster than most alternatives, see benchmarks.
- 📐 Lightweight (starts at ~80B)
- 🛡️ Tested on node 20-26, bun, browser, cloudflare workers and runtime/edge.
- 🙏 Works cross-realms (node:vm runInNewContext,...)
- 🗝️ ESM only, CommonJS can still
require()it on Node 20.19+/22.12+ (require(esm)), bun and bundlers.
Documentation
👉 Official website or GitHub Readme
Usage
isPlainObject
import { isPlainObject } from "@httpx/plain-object";
// ✅👇 True
isPlainObject({}); // ✅
isPlainObject({ key: "value" }); // ✅
isPlainObject({ key: new Date() }); // ✅
isPlainObject(new Object()); // ✅
isPlainObject(Object.create(null)); // ✅
isPlainObject({ nested: { key: true } }); // ✅
isPlainObject(new Proxy({}, {})); // ✅
isPlainObject({ [Symbol("tag")]: "A" }); // ✅
// ✅👇 (node context, workers, ...)
const runInNewContext = await import("node:vm").then(
(mod) => mod.runInNewContext
);
isPlainObject(runInNewContext("({})")); // ✅
// ❌👇 False
class Test {}
isPlainObject(new Test()); // ❌
isPlainObject(10); // ❌
isPlainObject(null); // ❌
isPlainObject("hello"); // ❌
isPlainObject([]); // ❌
isPlainObject(new Date()); // ❌
isPlainObject(new Uint8Array([1])); // ❌
isPlainObject(Buffer.from("ABC")); // ❌
isPlainObject(Promise.resolve({})); // ❌
isPlainObject(Object.create({})); // ❌
isPlainObject(new (class Cls {})()); // ❌
// ⚠️ Edge cases
//
// 👇 globalThis isn't properly portable across all JS environments
// In Bun, globalThis is a plain object, but in other environments
// (browser, Nodejs, edge, cloudflare, deno), it's not.
//
// 👇 process.env isn't properly portable across all JS environments
// In Bun && cloudflare, process.env is a plain object, but in other environments
// (browser, Nodejs, edge, cloudflare, deno), it's not.
isPlainObject(globalThis); // ✅ with Bun ❌ otherwise (browser, Nodejs, edge, cloudflare, deno)
isPlainObject(process.env); // ✅ with Bun and cloudflare ❌ otherwise (Nodejs, edge, deno)
// 👇 Static built-in namespaces aren't checked at runtime. This is a trade-off
// to maintain the best performance and size. When statically known, they
// are rejected at typecheck. If you need a runtime check, use a custom type
// guard. But in most cases, you won't need to check for these as the
// probability of writing a code that receives these as plain objects is low.
// and probably indicates an issue in your code.
isPlainObject(Math); // ⚠️✅ return true, typecheck error
isPlainObject(JSON); // ⚠️✅ return true, typecheck error
isPlainObject(Atomics); // ⚠️✅ return true, typecheck error
isPlainObject(Reflect); // ⚠️✅ return true, typecheck error
isPlainObject(Intl); // ⚠️✅ return true, typecheck error
isPlainObject(WebAssembly); // ⚠️✅ return true, typecheck error
isPlainObject(Temporal); // ⚠️✅ return true, typecheck error
isPlainObject(Temporal.Now); // ⚠️✅ return true, typecheck errorassertPlainObject
import { assertPlainObject } from "@httpx/plain-object";
import type { PlainObject } from "@httpx/plain-object";
function fn(value: unknown) {
// 👇 Throws `new TypeError('Not a PlainObject')` if not a plain object
assertPlainObject(value);
// 👇 Throws `new TypeError('Custom message')` if not a plain object
assertPlainObject(value, "Custom message");
// 👇 Throws custom error if not a plain object
assertPlainObject(value, () => {
throw new HttpBadRequest("Custom message");
});
return value;
}
try {
const value = fn({ key: "value" });
// ✅ Value is known to be PlainObject<unknown>
assertType<PlainObject>(value);
} catch (error) {
console.error(error);
}PlainObject type
Generic
isPlainObject and assertPlainObject accepts a generic to provide type autocompletion. Be aware that no runtime check are done. If you're looking for runtime validation, check zod, valibot or other alternatives.
import { isPlainObject } from "@httpx/plain-object";
import type { PlainObject } from "@httpx/plain-object";
type CustomType = {
id: number;
data?: {
test: string[];
attributes?: {
url?: string | null;
caption?: string | null;
alternativeText?: string | null;
} | null;
} | null;
};
const value = { id: 1 } as unknown;
if (isPlainObject<CustomType>(value)) {
// ✅ Value is a PlainObject with typescript autocompletion
// Note that there's no runtime checking of keys, so they are
// `unknown | undefined`. They will require unsing `?.` to access.
const url = value?.data?.attributes?.url; // autocompletion works
// ✅ url is `unknown | undefined`, so in order to use it, you'll need to
// manually check for the type.
if (typeof url === "string") {
console.log(url.toUpperCase());
}
}PlainObject
import { assertPlainObject } from "@httpx/plain-object";
import type { PlainObject } from "@httpx/plain-object";
function someFn(value: PlainObject) {
//
}
const value = { key: "value" } as unknown;
assertPlainObject(value);
someFn(value);Benchmarks
Performance is continuously monitored thanks to codspeed.io.
NodeJs 26.x
RUN v5.0.5 /Users/sebastien/github/httpx/packages/plain-object
✓ |bench| bench/comparative.bench.ts > Compare calling isPlainObject with 110x mixed types values > isPlainObject 14170ms
name hz min max mean p75 p99 p995 p999 rme samples
"zod":"4.6.7": 'zod.util.isPlainObject(v)' 2,263,343.78 0.0004 0.0855 0.0004 0.0005 0.0006 0.0006 0.0007 ±0.06% 2237029 fastest
"@sindresorhus/is":"8.1.0": 'is.plainObject(v)' 1,925,803.26 0.0004 0.0525 0.0005 0.0005 0.0008 0.0008 0.0009 ±0.06% 1891693
"is-plain-obj":"4.1.0": 'isPlainObj(v)' 1,919,024.52 0.0004 0.0567 0.0005 0.0005 0.0008 0.0008 0.0010 ±0.07% 1879232
"@httpx/plain-object": `isPlainObject(v)` 1,921,739.24 0.0004 0.0729 0.0005 0.0005 0.0008 0.0008 0.0020 ±0.09% 1870744
"es-toolkit":"1.52.0": 'isPlainObject(v)' 1,814,065.89 0.0005 0.0651 0.0006 0.0006 0.0007 0.0008 0.0009 ±0.05% 1790143
"is-plain-object":"5.1.0": 'isPlainObject(v)' 1,285,462.54 0.0007 2.9759 0.0008 0.0008 0.0011 0.0011 0.0059 ±0.59% 1253320
"redux":"5.0.1": 'isPlainObject(v)' 633,144.10 0.0014 0.0631 0.0016 0.0016 0.0021 0.0022 0.0070 ±0.06% 625439
"immer/is-plain-object":"4.2.0": 'isPlainObject(v)' 514,604.33 0.0017 0.0983 0.0020 0.0020 0.0027 0.0034 0.0125 ±0.11% 502725
lodash-es:"4.18.1": '_.isPlainObject(v)' 73,312.20 0.0128 2.4373 0.0139 0.0135 0.0220 0.0240 0.0416 ±0.51% 72115 slowestBun 1.4.3
RUN v5.0.5 /Users/sebastien/github/httpx/packages/plain-object
✓ |bench| bench/comparative.bench.ts > Compare calling isPlainObject with 110x mixed types values > isPlainObject 15707ms
name hz min max mean p75 p99 p995 p999 rme samples
"@httpx/plain-object": `isPlainObject(v)` 7,689,806.89 0.0000 0.1057 0.0001 0.0001 0.0002 0.0002 0.0003 ±0.06% 7507457 fastest
"@sindresorhus/is":"8.1.0": 'is.plainObject(v)' 7,229,347.44 0.0001 0.0615 0.0001 0.0002 0.0002 0.0003 0.0003 ±0.07% 6953226
"is-plain-obj":"4.1.0": 'isPlainObj(v)' 4,350,985.40 0.0002 0.0631 0.0002 0.0003 0.0003 0.0004 0.0005 ±0.07% 4237214
"redux":"5.0.1": 'isPlainObject(v)' 3,684,369.24 0.0002 0.0533 0.0003 0.0003 0.0004 0.0004 0.0005 ±0.06% 3608776
"es-toolkit":"1.52.0": 'isPlainObject(v)' 3,074,880.60 0.0002 0.0481 0.0003 0.0003 0.0005 0.0005 0.0006 ±0.05% 3023156
"zod":"4.6.7": 'zod.util.isPlainObject(v)' 2,289,090.91 0.0003 0.0606 0.0004 0.0005 0.0006 0.0006 0.0008 ±0.06% 2248832
"is-plain-object":"5.1.0": 'isPlainObject(v)' 1,770,925.19 0.0005 0.3979 0.0006 0.0006 0.0009 0.0010 0.0035 ±0.52% 1674197
"immer/is-plain-object":"4.2.0": 'isPlainObject(v)' 1,294,721.40 0.0007 0.0550 0.0008 0.0008 0.0010 0.0010 0.0012 ±0.06% 1280621
lodash-es:"4.18.1": '_.isPlainObject(v)' 296,159.72 0.0028 1.1251 0.0039 0.0034 0.0142 0.0163 0.0223 ±0.62% 259582 slowestSee benchmark file for details.
Bundle size
Bundle size is tracked by a size-limit configuration
| Scenario (esm) | Size (compressed) |
| -------------------------------------------------------- | ----------------: |
| import { isPlainObject } from '@httpx/plain-object | ~ 80B |
| import { assertPlainObject } from '@httpx/plain-object | ~ 133B |
| Both isPlainObject and assertPlainObject | ~ 141B |
Compatibility
| Level | CI | Description | | --- | --- | --- | | Node | ✅ | Node 20.19+. CI for 20.x, 22.x, 24.x & 26.x. | | Browser | ✅ | Tested with latest chrome (vitest/playwright) | | Browserslist | ✅ | > 95% on 01/2025. defaults, chrome >= 96, firefox >= 105, edge >= 113, safari >= 15, ios >= 15, opera >= 103, not dead | | Bun | ✅ | Tested with latest (at time of writing >= 1.3.3) | | Edge | ✅ | Ensured on CI with @vercel/edge-runtime. | | Cloudflare | ✅ | Ensured with @cloudflare/vitest-plugin (see wrangler.toml | | Typescript | ✅ | TS 5.7 + / are-the-type-wrong checks on CI. | | ES2022 | ✅ | Dist files checked with es-check | | Performance | ✅ | Monitored with codspeed.io |
For older browsers: most frontend frameworks can transpile the library (ie: nextjs...)
Comparison with other libraries
| Library | Compat | Perf (node 26 / bun 1.4) | | --- | --- | --- | | is-plain-obj | Differences | on par / 1.77x slower | | @sindresorhus/is | Differences | on par / 1.06x slower | | es-toolkit | No | 1.06x slower / 2.50x slower | | (@redux)isPlainObject | ✅ 100% | 3.04x slower / 2.09x slower | | (zod)util.isPlainObject | Differences | 1.18x faster / 3.36x slower | | (lodash)isPlainObject | No | 26.21x slower / 25.97x slower |
Measured with the benchmarks above (110 mixed values per call).
redux/isPlainObject
100% compatible see tests.
zod/util.isPlainObject
Zod is faster on node because it relies on the constructor property (cheap inline-cached property reads) rather than walking the prototype chain with Object.getPrototypeOf. The trade-off is that it can be fooled by inherited or user-defined constructor keys:
isPlainObject(Object.create({})); // ❌ false, zod ✅ true (prototype isn't Object.prototype)
isPlainObject({ constructor: class Cls {} }); // ✅ true, zod ❌ false (a regular data key)See zod proposal
@sindresorhus/is-plain-obj
This library wouldn't be possible without @sindresorhus is-plain-obj. Notable differences:
- [x] Faster on bun (1.77x), on par on node, and lighter
- [x] Named export.
- [x] Smaller bundle size.
- [x] Provide a
PlainObjecttype andassertPlainObjectfunction. - [x] Typescript convenience
PlainObjecttype.
Since v2, it diverges from is-plain-obj by
- [x] Static built-in namespaces (Math, JSON, Intl, Temporal...) are considered as plain objects at runtime, but rejected at typecheck when statically known.
- [x]
[Symbol.iterator]is considered as a valid property for plain objects. - [x]
[Symbol.toStringTag]is considered as a valid property for plain objects.`
Contributors
Contributions are welcome. Have a look to the CONTRIBUTING document.
Sponsors
If my OSS work brightens your day, let's take it to new heights together! Sponsor, coffee, or star – any gesture of support fuels my passion to improve. Thanks for being awesome! 🙏❤️
Special thanks to
License
MIT © Sébastien Vanvelthem and contributors.
