0.10.2 ā€¢ Published 4 days ago

@httpx/assert v0.10.2

Weekly downloads
-
License
MIT
Repository
github
Last release
4 days ago

@httpx/assert

Assertions and typeguards as primitives

npm changelog codecov bundles node browserslist size downloads license

warning: pre-v1, use at your own risks

Install

$ npm install @httpx/assert
$ yarn add @httpx/assert
$ pnpm add @httpx/assert

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.

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

assertStringNonEmpty(lang, () => new HttpBadRequest('Missing language'));

Usage

Type related

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

Object related

isPlainObject

NameTypeComment
isPlainObject<T?>PlainObject<T extends Record<string, unknown> = Record<string, unknown>
assertPlainObject<T?>PlainObject<T extends Record<string, unknown> = Record<string, unknown>
import { isPlainObject, assertPlainObject } from '@httpx/assert';

// Simple case: without generic value
isPlainObject({cwol: true}); // šŸ‘ˆ true
isPlainObject(new Promise()); // šŸ‘ˆ false
assertPlainObject({});

// 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 indicate not runtime check was done
}

Number related

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

Array related

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

String related

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 { assertStrParsableSafeInt, isStrParsableSafeInt } from '@httpx/assert';

isStrParsableSafeInt(2); // šŸ‘‰ false
isStrParsableSafeInt(`${Number.MAX_SAFE_INTEGER}`); // šŸ‘‰ true
assertStrParsableSafeInt(`${Number.MAX_SAFE_INTEGER}1`); // šŸ‘‰ throws

isParsableStrictIsoDateZ

Ensure a string contains a strict iso datetime with microseconds and utc suffix (aka: zulu time). Date is checked for validity.

NameTypeOpaque typeComment
isParsableStrictIsoDateZstringParsableStrictIsoDateZ
assertParsableStrictIsoDateZstringParsableStrictIsoDateZ
import { isParsableStrictIsoDateZ, assertParsableStrictIsoDateZ, type ParsableStrictIsoDateZ } from '@httpx/assert';

isParsableStrictIsoDateZ(new Date().toISOString()); // šŸ‘‰ true
isParsableStrictIsoDateZ('2023-12-29T23:37:31.653z'); // šŸ‘‰ true
isParsableStrictIsoDateZ('2023-02-29T23:37:31.653'); // šŸ‘‰ false, cause no 29th february in 2023

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
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~ 56b
Import isUuid~ 175b
Import isEan13~ 117b
All typeguards, assertions and helpers~ 900b

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.
ES2021āœ…Dist files checked with es-check
Node16Node 16.x supported, not ensured on CI

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

License

MIT Ā© belgattitude and contributors.

0.10.0

4 days ago

0.10.1

4 days ago

0.10.2

4 days ago

0.9.1

6 days ago

0.9.0

9 days ago

0.8.1

10 days ago

0.8.0

1 month ago

0.7.0

4 months ago

0.6.7

4 months ago

0.6.5

4 months ago

0.6.4

4 months ago

0.6.2

4 months ago

0.6.1

4 months ago

0.6.0

4 months ago

0.3.0

4 months ago

0.5.0

4 months ago

0.4.0

4 months ago

0.5.2

4 months ago

0.5.1

4 months ago

0.2.0

4 months ago

0.1.0

4 months ago