UNPKG

@httpx/assert

Version:
519 lines (389 loc) • 22.2 kB
# @httpx/assert Assertions and typeguards as primitives [![npm](https://img.shields.io/npm/v/@httpx/assert?style=for-the-badge&label=Npm&labelColor=444&color=informational)](https://www.npmjs.com/package/@httpx/assert) [![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/assert/CHANGELOG.md) [![codecov](https://img.shields.io/codecov/c/github/belgattitude/httpx?logo=codecov&label=Unit&flag=httpx-assert-unit&style=for-the-badge&labelColor=444)](https://app.codecov.io/gh/belgattitude/httpx/tree/main/packages%2Fassert) [![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/assert/.size-limit.cjs) [![node](https://img.shields.io/static/v1?label=Node&message=20%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/assert@latest?label=Max&style=for-the-badge&labelColor=444&color=informational)](https://bundlephobia.com/package/@httpx/assert@latest) [![downloads](https://img.shields.io/npm/dm/@httpx/assert?style=for-the-badge&labelColor=444)](https://www.npmjs.com/package/@httpx/assert) [![license](https://img.shields.io/npm/l/@httpx/assert?style=for-the-badge&labelColor=444)](https://github.com/belgattitude/httpx/blob/main/LICENSE) > **warning**: pre-v1, use at your own risks ## Install ```bash $ npm install @httpx/assert $ yarn add @httpx/assert $ pnpm add @httpx/assert ``` ## Features - šŸ‘‰&nbsp; Typeguards and assertions with a consistent style. - šŸ‘‰&nbsp; Assertions with useful [default error message](#assertions-error-messages). - šŸ‘‰&nbsp; Return *weak* [opaque types](#weak-opaque-types) for boolean, strings and numbers. - šŸ‘‰&nbsp; Optimized tree-shakability, starts at [56b](#bundle-size). - šŸ‘‰&nbsp; Don't leak values in the default assertion error messages. - šŸ‘‰&nbsp; No deps. [Node, browser and edge support](#compatibility). ## Documentation šŸ‘‰ [Official website](https://belgattitude.github.io/httpx/assert), [GitHub Readme](https://github.com/belgattitude/httpx/tree/main/packages/assert#readme) or [generated api doc](https://github.com/belgattitude/httpx/blob/main/packages/assert/docs/api/README.md) --- - [Introduction](#introduction) * [Consistent style](#consistent-style) * [Weak opaque types](#weak-opaque-types) * [Assertions error messages](#assertions-error-messages) - [Usage](#usage) * [Type related](#type-related) + [assertNever](#assertnever) * [Object related](#object-related) + [isPlainObject](#isplainobject) * [Number related](#number-related) + [isNumberSafeInt](#isnumbersafeint) * [Array related](#array-related) + [ArrayNonEmpty](#arraynonempty) * [String related](#string-related) + [StringNonEmpty](#stringnonempty) + [ParsableSafeInt](#parsablesafeint) + [isParsableStrictIsoDateZ](#isparsablestrictisodatez) * [Uuid](#uuid) + [isUuid](#isuuid) * [Barcode](#barcode) + [isEan13](#isean13) * [Network](#network) + [isNetWorkPort](#isnetworkport) * [Http](#http) + [isHttpMethod](#ishttpmethod) + [isValidHttpMethod](#isvalidhttpmethod) - [Bundle size](#bundle-size) - [Compatibility](#compatibility) - [Acknowledgments](#acknowledgments) - [Contributors](#contributors) - [Sponsors](#sponsors) ## Introduction ### Consistent style Typeguards starts with `isXXX` and have an assertion counterpart named `assertXXX`. `isParsableXXX` and `assertParsableXXX` denotes a string. ### Weak opaque types For `string`, `number` and `boolean` the returned type is tagged with a *weak* opaque type. It can optionally be used to enforce that the value was checked. For example: ```typescript import { assertUuidV7, type UuidV7 } from '@httpx/assert'; import { HttpUnprocessableEntity } from '@httpx/exception'; const persistRecord = async (uuid: UuidV7) => { // uuid is compatible with string. return await db.raw(`insert into tbl(uuid) values (${uuid})`) } const v = 'xxx'; // unknown assertUuidV7(v, () => new HttpUnprocessableEntity()); // šŸ‘‰ v is known to be `string & WeakOpaqueContainer<'UuidV4'>` await persistRecord(v); // will work await persistRecord('a_string'); // won't ``` ### Assertions error messages When an assertion fail, a native [TypeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError) is thrown by default with a message indicating the requirement and and information about the tested value. As an example: ```typescript expect(() => assertUuid('123')).toThrow( new TypeError('Value is expected to be an uuid, got: string(length:3)') ); expect(() => assertUuid(false, undefined, { version: 1 })).toThrow( new TypeError('Value is expected to be an uuid v1, got: boolean(false)') ); expect(() => assertUuidV1(Number.NaN)).toThrow( new TypeError('Value is expected to be an uuid v1, got: NaN') ); expect(() => assertUuidV3(new Error())).toThrow( new TypeError('Value is expected to be an uuid v3, got: Error') ); expect(() => assertUuidV4(new Date())).toThrow( new TypeError('Value is expected to be an uuid v4, got: Date') ); expect(() => assertUuidV5(() => {})).toThrow( new TypeError('Value is expected to be an uuid v5, got: function') ); expect(() => assertUuidV7(() => {})).toThrow( new TypeError('Value is expected to be an uuid v7, got: function') ); //... ``` Alternatively it's possible to provide either a message or function returning an Error. For example: ```typescript import { assertEan13, assertStringNonEmpty } from '@httpx/assert'; import { HttpBadRequest } from '@httpx/exception'; assertEan13('123', 'Not a barcode'); // šŸ‘ˆ Will throw a TypeError('Not a barcode') const lang = null; assertStringNonEmpty(lang, () => new HttpBadRequest('Missing language')); ``` ## Usage ### Type related #### assertNever ```typescript import { assertNever } from '@httpx/assert'; type PromiseState = 'resolved' | 'rejected' | 'running' const state: PromiseState = 'rejected'; switch(state) { case 'resolved': return v; case 'rejected': return new Error(); default: assertNever(state); // šŸ‘ˆ TS will complain about missing 'running' state // ā˜ļø Will throw a TypeError in js. } ``` > PS: you can use the `assertNeverNoThrow` with the same behaviour except that it > doesn't throw and return the value instead (no runtime error). ### Object related #### isPlainObject | Name | Type | Comment | |-------------------------|------------------|---------| | isPlainObject\<T?\> | `PlainObject` | | | assertPlainObject\<T?\> | `PlainObject` | | > Based on [@httpx/plain-object](https://github.com/belgattitude/httpx/tree/main/packages/plain-object#readme) ```typescript import { isPlainObject, assertPlainObject } from '@httpx/assert'; // āœ…šŸ‘‡ True 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(Math); // āŒ Static built-in classes isPlainObject(Promise.resolve({})); // āŒ isPlainObject(Object.create({})); // āŒ assertPlainObject({}) // šŸ‘ˆ āœ… true ``` #### Usage with generic ```typescript import { isPlainObject, assertPlainObject } from '@httpx/assert'; // With generic value (unchecked at runtime!) type CustomType = { name: string; deep: { yes: boolean | null; }; }; const value = { name: 'hello', deep: { yes: true, }, } as unknown; if (isPlainObject<CustomType>(value)) { // Notice it's a deep partial to allow autocompletion value?.deep?.yes; // šŸ‘ˆ yes will be unknown to reflect that no runtime check was done } assertPlainObject<CustomType>(value); ``` ### Number related #### isNumberSafeInt ```typescript import { assertNumberSafeInt, isNumberSafeInt } from '@httpx/assert'; isNumberSafeInt(10n); // šŸ‘‰ false isNumberSafeInt(BigInt(10)); // šŸ‘‰ false isNumberSafeInt(Number.MAX_SAFE_INTEGER); // šŸ‘‰ true assertNumberSafeInt(Number.MAX_SAFE_INTEGER + 1); // šŸ‘‰ throws ``` ### Array related #### ArrayNonEmpty | Name | Type | Opaque type | Comment | |---------------------|-------------|-----------------|-----------------| | isArrayNonEmpty | `unknown[]` | `ArrayNonEmpty` | | | assertArrayNonEmpty | `unknown[]` | `ArrayNonEmpty` | | ```typescript import { isArrayNonEmpty, assertArrayNonEmpty, type ArrayNonEmpty } from '@httpx/assert'; isArrayNonEmpty([]) // šŸ‘‰ false isArrayNonEmpty([0,1]) // šŸ‘‰ true isArrayNonEmpty([null]) // šŸ‘‰ true assertArrayNonEmpty([]) // šŸ‘‰ throws ``` ### String related #### StringNonEmpty | Name | Type | Opaque type | Comment | |----------------------|-----------|------------------|-----------------| | isStringNonEmpty | `string` | `StringNonEmpty` | Trims the value | | assertStringNonEmpty | `string` | `StringNonEmpty` | Trims the value | ```typescript import { assertStringNonEmpty, isStringNonEmpty, type StringNonEmpty } from '@httpx/assert'; isStringNonEmpty(''); // šŸ‘‰ false isStringNonEmpty(' '); // šŸ‘‰ false: trim by default assertStringNonEmpty(''); // šŸ‘‰ throws ``` #### ParsableSafeInt | Name | Type | Opaque type | Comment | |-----------------------|-----------|-------------------|-----------------| | isParsableSafeInt | `string` | `ParsableSafeInt` | | | assertParsableSafeInt | `string` | `ParsableSafeInt` | | ```typescript import { assertParsableSafeInt, isParsableSafeInt } from '@httpx/assert'; isParsableSafeInt(2); // šŸ‘‰ false isParsableSafeInt(`${Number.MAX_SAFE_INTEGER}`); // šŸ‘‰ true assertParsableSafeInt(`${Number.MAX_SAFE_INTEGER}1`); // šŸ‘‰ throws ``` #### isParsableStrictIsoDateZ Check if a value is a string that contains an ISO-8601 date time in 'YYYY-MM-DDTHH:mm:ss.sssZ' format (UTC+0 / time). This check allow the value to be safely passed to `new Date()`or `Date.parse()` without parser or timezone mis-interpretations. 'T' and 'Z' checks are done in a case-insensitive way. | Name | Type | Opaque type | Comment | |------------------------------|-----------|--------------------------|-----------------| | isParsableStrictIsoDateZ | `string` | `ParsableStrictIsoDateZ` | | | assertParsableStrictIsoDateZ | `string` | `ParsableStrictIsoDateZ` | | ```typescript import { isParsableStrictIsoDateZ, assertParsableStrictIsoDateZ, type ParsableStrictIsoDateZ } from '@httpx/assert'; isParsableStrictIsoDateZ(new Date().toISOString()); // āœ… true isParsableStrictIsoDateZ('2023-12-28T23:37:31.653Z'); // āœ… true isParsableStrictIsoDateZ('2023-12-29T23:37:31.653z'); // āœ… true (case-insensitive works) isParsableStrictIsoDateZ('2023-12-28T23:37:31.653'); // āŒ false (missing 'Z') isParsableStrictIsoDateZ('2023-02-29T23:37:31.653Z'); // āŒ false (No 29th february in 2023) // assertion const dateStr = '2023-12-29T23:37:31.653Z'; assertParsableStrictIsoDateZ(dateStr, `Invalid date: ${dateStr}`); // šŸ‘‰ assertion passed, safe to use -> ParsableStrictIsoDateZ const date = new Date(dateStr); const timestampNumber = Date.parse(dateStr); assertParsableStrictIsoDateZ('2023-02-29T23:37:31.653Z'); // šŸ’„ throws cause no 29th february ``` ### Uuid #### isUuid | Name | Type | Opaque type | Comment | |----------------|-------------------------|--------------------------------------------------|--------| | isUuid | `string` | `UuidV1 \| UuidV3 \| UuidV4 \| UuidV5 \| UuidV7` | | | isUuidV1 | `string` | `UuidV1` | | | isUuidV3 | `string` | `UuidV3` | | | isUuidV4 | `string` | `UuidV4` | | | isUuidV5 | `string` | `UuidV5` | | | isUuidV7 | `string` | `UuidV7` | | | assertUuid | `string` | `UuidV1 \| UuidV3 \| UuidV4 \| UuidV5 \| UuidV7` | | | assertUuidV1 | `string` | `UuidV5` | | | assertUuidV3 | `string` | `UuidV3` | | | assertUuidV4 | `string` | `UuidV4` | | | assertUuidV5 | `string` | `UuidV5` | | | assertUuidV7 | `string` | `UuidV7` | | | getUuidVersion | `1 \| 3 \| 4 \| 5 \| 7` | | | ```typescript import { isUuid, isUuidV1, isUuidV3, isUuidV4, isUuidV5 } from "@httpx/assert"; import { assertUuid, assertUuidV1, assertUuidV3, assertUuidV4, assertUuidV5 } from "@httpx/assert"; import { getUuidVersion } from '@httpx/assert'; // Without version isUuid('90123e1c-7512-523e-bb28-76fab9f2f73d'); // šŸ‘‰ valid uuid v1, 3, 4 or 5 assertUuid('90123e1c-7512-523e-bb28-76fab9f2f73d'); // With version assertUuid('90123e1c-7512-523e-bb28-76fab9f2f73d'); assertUuidV5('90123e1c-7512-523e-bb28-76fab9f2f73d') isUuid('90123e1c-7512-523e-bb28-76fab9f2f73d'); isUuidV4('d9428888-122b-11e1-b85c-61cd3cbb3210'); // šŸ‘ˆ or isUuidV1(''), isUuidV3(''), isUuidV5('')...; // Utils getUuidVersion('90123e1c-7512-523e-bb28-76fab9f2f73d'); // 5 ``` ### Barcode #### isEan13 Supported barcodes is currently limited to Ean13 ```typescript import { isEan13 } from "@httpx/assert"; import { assertEan13 } from "@httpx/assert"; isEan13('1234567890128'); // šŸ‘ˆ will check digit too assertEan13('1234567890128'); ``` ### Network #### isNetWorkPort Check whether the value is a valid tcp/udp network port (0-65535) ```typescript import { isNetworkPort } from "@httpx/assert"; import { assertNetworkPort } from "@httpx/assert"; import { type NetworkPort } from "@httpx/assert"; isNetworkPort(443); // šŸ‘ˆ weak opaque type is NetworkPort assertNetworkPort(443); ``` ### Http #### isHttpMethod Check whether the value is a specific http method (case-insensitive). ```typescript import { isHttpMethod } from "@httpx/assert"; import { assertHttpMethod } from "@httpx/assert"; import { type HttpMethod } from "@httpx/assert"; const value: unknown = 'GET'; isHttpMethod('GET', value); // šŸ‘ˆ weak opaque type is HttpMethod assertHttpMethod('GET', value); ``` #### isValidHttpMethod Check whether the value is a valid http method (case-insensitive). ```typescript import { isHttpValidMethod } from "@httpx/assert"; import { assertHttpValidMethod } from "@httpx/assert"; import { type HttpMethod } from "@httpx/assert"; const value: unknown = 'GET'; isHttpValidMethod(value); // šŸ‘ˆ weak opaque type is HttpMethod assertHttpValidMethod(value); ``` ## Bundle size Code and bundler have been tuned to target a minimal compressed footprint for the browser. ESM individual imports are tracked by a [size-limit configuration](https://github.com/belgattitude/httpx/blob/main/packages/assert/.size-limit.cjs). | Scenario | Size (compressed) | |----------------------------------------|------------------:| | Import `isPlainObject` | ~ 100b | | Import `isUuid` | ~ 175b | | Import `isEan13` | ~ 117b | | All typeguards, assertions and helpers | ~ 1700b | > For CJS usage (not recommended) track the size on [bundlephobia](https://bundlephobia.com/package/@httpx/assert@latest). ## Compatibility | Level | CI | Description | |--------------|----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Node | āœ… | CI for 20.x, 22.x & 24.x. | | Browser | āœ… | Tested with latest chrome (vitest/playwright) | | Browserslist | āœ… | [> 95%](https://browserslist.dev/?q=ZGVmYXVsdHMsIGNocm9tZSA%2BPSA5NiwgZmlyZWZveCA%2BPSAxMDUsIGVkZ2UgPj0gMTEzLCBzYWZhcmkgPj0gMTUsIGlvcyA%2BPSAxNSwgb3BlcmEgPj0gMTAzLCBub3QgZGVhZA%3D%3D) on 01/2025. [defaults, chrome >= 96, firefox >= 105, edge >= 113, safari >= 15, ios >= 15, opera >= 103, not dead](https://github.com/belgattitude/httpx/blob/main/packages/assert/.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) | > For _older_ browsers: most frontend frameworks can transpile the library (ie: [nextjs](https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages)...) ## Acknowledgments Special thanks for inspiration: - [sindresorhus/is](https://github.com/sindresorhus/is) - [webmozarts/assert](https://github.com/webmozarts/assert) ## Contributors Contributions are warmly appreciated. 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.