belgattitude/httpx

View on GitHub
packages/plain-object/README.md

Summary

Maintainability
Test Coverage
# @httpx/plain-object

[Fast](#benchmarks) and lightweight ([~80B](#bundle-size)) 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](#comparison-with-other-libraries).

[![npm](https://img.shields.io/npm/v/@httpx/plain-object?style=for-the-badge&label=Npm&labelColor=444&color=informational)](https://www.npmjs.com/package/@httpx/plain-object)
[![changelog](https://img.shields.io/static/v1?label=&message=changelog&logo=github&style=for-the-badge&labelColor=444&color=informational)](https://github.com/belgattitude/httpx/blob/main/packages/plain-object/CHANGELOG.md)
[![codecov](https://img.shields.io/codecov/c/github/belgattitude/httpx?logo=codecov&label=Unit&flag=httpx-plain-object-unit&style=for-the-badge&labelColor=444)](https://app.codecov.io/gh/belgattitude/httpx/tree/main/packages%2Fplain-object)
[![bundles](https://img.shields.io/static/v1?label=&message=cjs|esm@treeshake&logo=webpack&style=for-the-badge&labelColor=444&color=informational)](https://github.com/belgattitude/httpx/blob/main/packages/plain-object/.size-limit.cjs)
[![node](https://img.shields.io/static/v1?label=Node&message=18%2b&logo=node.js&style=for-the-badge&labelColor=444&color=informational)](#compatibility)
[![browserslist](https://img.shields.io/static/v1?label=Browser&message=%3E96%25&logo=googlechrome&style=for-the-badge&labelColor=444&color=informational)](#compatibility)
[![size](https://img.shields.io/bundlephobia/minzip/@httpx/plain-object@latest?label=Max&style=for-the-badge&labelColor=444&color=informational)](https://bundlephobia.com/package/@httpx/plain-object@latest)
[![downloads](https://img.shields.io/npm/dm/@httpx/plain-object?style=for-the-badge&labelColor=444)](https://www.npmjs.com/package/@httpx/plain-object)
[![license](https://img.shields.io/npm/l/@httpx/plain-object?style=for-the-badge&labelColor=444)](https://github.com/belgattitude/httpx/blob/main/LICENSE)

## Install

```bash
$ npm install @httpx/plain-object
$ yarn add @httpx/plain-object
$ pnpm add @httpx/plain-object
```

## Features

- πŸ‘‰  Provide [isPlainObject](#isplainobject) and [assertPlainObject](#assertplainobject) functions.
- πŸ¦„  Convenience [PlainObject](#plainobject-type) typescript typings.
- πŸš€  Faster than most alternatives, see [benchmarks](#benchmarks).
- πŸ“  Lightweight (starts at [~80B](#bundle-size)) 
- πŸ›‘οΈ  Tested on [node 18-22, browser, cloudflare workers and runtime/edge](#compatibility).
- πŸ™  Cross-realms tolerant (node:vm runInNewContext,...)
- πŸ—οΈ  Available in ESM and CJS formats.

## Documentation

πŸ‘‰ [Official website](https://belgattitude.github.io/httpx/plain-object) or [GitHub Readme](https://github.com/belgattitude/httpx/tree/main/packages/plain-object#readme)

## Usage

### isPlainObject

```typescript
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('({})'));   // βœ…

// βœ…πŸ‘‡ Static built-in classes are treated as plain objects
//       check for `isStaticBuiltInClass` to exclude if needed

isPlainObject(Math);                // βœ…
isPlainObject(JSON);                // βœ…
isPlainObject(Atomics);             // βœ…

// βŒπŸ‘‡ 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 {}));  // ❌
isPlainObject(globalThis);          // ❌,
```

### assertPlainObject

```typescript
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);
}
```

### isStaticBuiltInClass

> info: Since v2.0.0

Since v2.0.0, `isPlainObject` will accept static built-in classes 
as plain objects (Math, JSON, Atomics). If you need to exclude them,
a new typeguard has been created `isStaticBuiltInClass`.

```typescript
import { isPlainObject, isStaticBuiltInClass } from '@httpx/plain-object';
const v = Math; // or Atomics or JSON
if (isPlainObject(v) && !isStaticBuiltInClass(v)) {
    console.log('v is a plain object but not a static built-in class');
}
```
### PlainObject type

#### Generic

`Γ¬sPlainObject` 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.

```typescript
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

```typescript
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](https://codspeed.io/belgattitude/httpx). 
>
> [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/belgattitude/httpx)

```
 RUN  v2.1.2 /home/sebastien/github/httpx/packages/plain-object
 
 βœ“ bench/comparative.bench.ts (7) 5269ms
   βœ“ Compare calling isPlainObject with 110x mixed types values (7) 5267ms
     name                                                         hz     min     max    mean     p75     p99    p995    p999     rme  samples
   Β· @httpx/plain-object: `isPlainObject(v)`              948,266.97  0.0010  0.0700  0.0011  0.0011  0.0013  0.0017  0.0034  Β±0.10%   474134   fastest
   Β· (sindresorhus/)is-plain-obj: `isPlainObj(v)`         796,997.41  0.0011  0.0718  0.0013  0.0012  0.0026  0.0028  0.0053  Β±0.14%   398499
   Β· @sindresorhus/is: `is.plainObject(v)`                544,333.86  0.0017  0.0359  0.0018  0.0018  0.0032  0.0037  0.0083  Β±0.11%   272167
   Β· estoolkit:  `isPlainObject(v)`                        87,689.00  0.0103  1.0569  0.0114  0.0112  0.0189  0.0208  0.0350  Β±0.47%    43845
   Β· redux:  `isPlainObject(v)`                           257,141.86  0.0030  0.3135  0.0039  0.0038  0.0070  0.0094  0.0219  Β±0.23%   128571
   Β· (jonschlinkert/)is-plain-object: `isPlainObject(v)`  403,749.92  0.0018  0.3947  0.0025  0.0024  0.0057  0.0083  0.0185  Β±0.38%   201875
   Β· lodash-es: `_.isPlainObject(v)`                        9,949.41  0.0685  0.6957  0.1005  0.1101  0.1964  0.2144  0.2790  Β±0.95%     4975   slowest


 BENCH  Summary

  @httpx/plain-object: `isPlainObject(v)` - bench/comparative.bench.ts > Compare calling isPlainObject with 110x mixed types values
    1.19x faster than (sindresorhus/)is-plain-obj: `isPlainObj(v)`
    1.74x faster than @sindresorhus/is: `is.plainObject(v)`
    2.35x faster than (jonschlinkert/)is-plain-object: `isPlainObject(v)`
    3.69x faster than redux:  `isPlainObject(v)`
    10.81x faster than estoolkit:  `isPlainObject(v)`
    95.31x faster than lodash-es: `_.isPlainObject(v)`

```

> See [benchmark file](https://github.com/belgattitude/httpx/blob/main/packages/plain-object/bench/comparative.bench.ts) for details.

## Bundle size

Bundle size is tracked by a [size-limit configuration](https://github.com/belgattitude/httpx/blob/main/packages/plain-object/.size-limit.cjs)

| Scenario (esm)                                              | Size (compressed) |
|-------------------------------------------------------------|------------------:|
| `import { isPlainObject } from '@httpx/plain-object`        |             ~ 80B |
| `import { assertPlainObject } from '@httpx/plain-object`    |            ~ 134B |
| `Both isPlainObject and assertPlainObject`                  |            ~ 142B |
| `import { isStaticBuiltInClass } from '@httpx/plain-object` |             ~ 37B |

> For CJS usage (not recommended) track the size on [bundlephobia](https://bundlephobia.com/package/@httpx/plain-object@latest).

## Compatibility

| Level      | CI | Description                                                                                                                                                                                                                                                                                                                                    |
|------------|----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|  
| Node       | βœ…  | CI for 18.x, 20.x & 22.x.                                                                                                                                                                                                                                                                                                                      |
| Browser      | βœ…  | Tested with latest chrome (vitest/playwright)                                                                                                                                                                                                                                                                                            |
| Browsers   | βœ…  | [> 96%](https://browserslist.dev/?q=ZGVmYXVsdHMsIGNocm9tZSA%2BPSA5NixmaXJlZm94ID49IDkwLGVkZ2UgPj0gMTksc2FmYXJpID49IDEyLGlvcyA%2BPSAxMixvcGVyYSA%2BPSA3Nw%3D%3D) on 07/2024. Mins to [Chrome 96+, Firefox 90+, Edge 19+, iOS 12+, Safari 12+, Opera 77+](https://github.com/belgattitude/httpx/blob/main/packages/plain-object/.browserslistrc) |
| Edge         | βœ…  | Ensured on CI with [@vercel/edge-runtime](https://github.com/vercel/edge-runtime).                                                                                                                                                                                                                                                       | 
| Cloudflare   | βœ…  | Ensured with @cloudflare/vitest-pool-workers (see [wrangler.toml](https://github.com/belgattitude/httpx/blob/main/devtools/vitest/wrangler.toml)                                                                                                                                                                                         |
| Typescript | βœ…  | TS 5.0 + / [are-the-type-wrong](https://github.com/arethetypeswrong/arethetypeswrong.github.io) checks on CI.                                                                                                                                                                                                                                  |
| ES2022     | βœ…  | Dist files checked with [es-check](https://github.com/yowainwright/es-check)                                                                                                                                                                                                                                                                   |
| Performance| βœ…  | Monitored with [codspeed.io](https://codspeed.io/belgattitude/httpx)                                                                                                                                                                                                                                                                      |

> For _older_ browsers: most frontend frameworks can transpile the library (ie: [nextjs](https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages)...)

## Comparison with other libraries

| Library                                                        | Compat      | Perf         | CJS+ESM |
|----------------------------------------------------------------|-------------|--------------|---------|  
| [is-plain-obj](https://github.com/sindresorhus/is-plain-obj)   | Differences | 1.09x slower | No      | 
| [es-toolkit](https://github.com/toss/es-toolkit)               | No          |              | Yes     | 
| (@redux)[isPlainObject](https://github.com/reduxjs/redux)      | βœ… 100%      | 2.80x slower | Yes     |

### redux/isPlainObject

100% compatible see tests.

### @sindresorhus/is-plain-obj

This library wouldn't be possible without [@sindresorhus](https://github.com/sindresorhus) [is-plain-obj](https://github.com/sindresorhus/is-plain-obj).
Notable differences:

- [x] SLightly faster (10%)
- [x] ESM and CJS formats.
- [x] Named export.
- [x] Smaller bundle size.
- [x] Provide a `PlainObject` type and `assertPlainObject` function.
- [x] Typescript convenience `PlainObject` type.

Since v2, it diverges from `is-plain-obj` by 

- [x] Static built-in classes are considered as plain objects (use [isStaticBuiltInClass](#isstaticbuiltinclass) to exclude).
- [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](https://github.com/belgattitude/httpx/blob/main/CONTRIBUTING.md) document.

## Sponsors

If my OSS work brightens your day, let's take it to new heights together!
[Sponsor](<[sponsorship](https://github.com/sponsors/belgattitude)>), [coffee](<(https://ko-fi.com/belgattitude)>),
or star – any gesture of support fuels my passion to improve. Thanks for being awesome! πŸ™β€οΈ

### Special thanks to

<table>
  <tr>
    <td>
      <a href="https://www.jetbrains.com/?ref=belgattitude" target="_blank">
         <img width="65" src="https://asset.brandfetch.io/idarKiKkI-/id53SttZhi.jpeg" alt="Jetbrains logo" />
      </a>
    </td>
    <td>
      <a href="https://www.embie.be/?ref=belgattitude" target="_blank">
        <img width="65" src="https://avatars.githubusercontent.com/u/98402122?s=200&v=4" alt="Jetbrains logo" />    
      </a>
    </td>
  </tr>
  <tr>
    <td align="center">
      <a href="https://www.jetbrains.com/?ref=belgattitude" target="_blank">JetBrains</a>
    </td>
    <td align="center">
      <a href="https://www.embie.be/?ref=belgattitude" target="_blank">Embie.be</a>
    </td>
   </tr>
</table>

## License

MIT Β© [belgattitude](https://github.com/belgattitude) and contributors.