@httpx/assertGetting started

Assertions and typeguards as primitives

npm changelog codecov bundles node browserslist size downloads license

Features

  • πŸ‘‰Β  Typeguards and assertions with a consistent style.
  • πŸ‘‰Β  Assertions with useful default error message.
  • πŸ‘‰Β  Return weak opaque types for boolean, strings and numbers.
  • πŸ‘‰Β  Optimized tree-shakability, starts at 56b.
  • πŸ‘‰Β  Don’t leak values in the default assertion error messages.
  • πŸ‘‰Β  No deps. Node, browser and edge support.

Install

yarn add @httpx/assert

Documentation

πŸ‘‰ Official website, GitHub Readme or generated api doc

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:

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 is thrown by default with a message indicating the requirement and and information about the tested value. As an example:

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:

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

assertNever

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).

isPlainObject

NameTypeComment
isPlainObject<T?>PlainObject
assertPlainObject<T?>PlainObject

Inspired and compatible with is-plain-obj. Check the test file

 
import { isPlainObject, assertPlainObject } from '@httpx/assert';
 
// Simple case: without generic value
isPlainObject({ });                     // πŸ‘ˆ βœ… true
isPlainObject({ key: 'value' });        // πŸ‘ˆ βœ… true
isPlainObject({ key: new Date() });     // πŸ‘ˆ βœ… true
isPlainObject(new Object());            // πŸ‘ˆ βœ… true
isPlainObject(Object.create(null));     // πŸ‘ˆ βœ… true
isPlainObject({nested: { key: true} }); // πŸ‘ˆ βœ… true
isPlainObject(runInNewContext('({})')); // πŸ‘ˆ βœ… true
 
class Test { };
 
isPlainObject(new Test())               // πŸ‘ˆ ❌ false
isPlainObject(10);                      // πŸ‘ˆ ❌ false
isPlainObject(null);                    // πŸ‘ˆ ❌ false
isPlainObject('hello');                 // πŸ‘ˆ ❌ false
isPlainObject([]);                      // πŸ‘ˆ ❌ false
isPlainObject(new Date());              // πŸ‘ˆ ❌ false
isPlainObject(Math);                    // πŸ‘ˆ ❌ false
// (... see test file)
 
assertPlainObject({})                  // πŸ‘ˆ βœ… true
 

Usage with generic

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);

isNumberSafeInt

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

ArrayNonEmpty

NameTypeOpaque typeComment
isArrayNonEmptyunknown[]ArrayNonEmpty
assertArrayNonEmptyunknown[]ArrayNonEmpty
import { isArrayNonEmpty, assertArrayNonEmpty, type ArrayNonEmpty } from '@httpx/assert';
 
isArrayNonEmpty([]) // πŸ‘‰ false
isArrayNonEmpty([0,1]) // πŸ‘‰ true
isArrayNonEmpty([null]) // πŸ‘‰ true
assertArrayNonEmpty([]) // πŸ‘‰ throws

StringNonEmpty

NameTypeOpaque typeComment
isStringNonEmptystringStringNonEmptyTrims the value
assertStringNonEmptystringStringNonEmptyTrims the value
import { assertStringNonEmpty, isStringNonEmpty, type StringNonEmpty } from '@httpx/assert';
 
isStringNonEmpty(''); // πŸ‘‰ false
isStringNonEmpty(' '); // πŸ‘‰ false: trim by default
assertStringNonEmpty(''); // πŸ‘‰ throws

ParsableSafeInt

NameTypeOpaque typeComment
isParsableSafeIntstringParsableSafeInt
assertParsableSafeIntstringParsableSafeInt
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.

NameTypeOpaque typeComment
isParsableStrictIsoDateZstringParsableStrictIsoDateZ
assertParsableStrictIsoDateZstringParsableStrictIsoDateZ
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

NameTypeOpaque typeComment
isUuidstringUuidV1 | UuidV3 | UuidV4 | UuidV5 | UuidV7
isUuidV1stringUuidV1
isUuidV3stringUuidV3
isUuidV4stringUuidV4
isUuidV5stringUuidV5
isUuidV7stringUuidV7
assertUuidstringUuidV1 | UuidV3 | UuidV4 | UuidV5 | UuidV7
assertUuidV1stringUuidV5
assertUuidV3stringUuidV3
assertUuidV4stringUuidV4
assertUuidV5stringUuidV5
assertUuidV7stringUuidV7
getUuidVersion1 | 3 | 4 | 5 | 7
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

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)

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).

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).

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.

ScenarioSize (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.

Compatibility

LevelCIDescription
Nodeβœ…CI for 18.x, 20.x & 22.x.
Browsersβœ…> 95% on 12/2023. Mins to Chrome 96+, Firefox 90+, Edge 19+, iOS 12+, Safari 12+, Opera 77+
Edgeβœ…Ensured on CI with @vercel/edge-runtime.
Typescriptβœ…TS 5.0+ / are-the-type-wrong checks on CI.
ES2022βœ…Dist files checked with es-check

For older browsers: most frontend frameworks can transpile the library (ie: nextjs…)

Acknowledgments

Special thanks for inspiration:

Contributors

Contributions are warmly appreciated. 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

Jetbrains logoJetbrains logo
JetBrainsEmbie.be

License

MIT Β© belgattitude and contributors.