0.15.1 • Published 8 months ago

@httpx/assert v0.15.1

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months 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')

const lang = null;
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
assertPlainObject\<T?>PlainObject

Based on @httpx/plain-object

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

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

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 { 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.
Browserāœ…Tested with latest chrome (vitest/playwright)
Browserslistāœ…> 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.
Cloudflareāœ…Ensured with @cloudflare/vitest-pool-workers (see wrangler.toml
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

License

MIT Ā© belgattitude and contributors.

0.12.0

1 year ago

0.13.0

9 months ago

0.12.1

1 year ago

0.14.0

8 months ago

0.13.1

9 months ago

0.12.2

1 year ago

0.15.0

8 months ago

0.13.2

8 months ago

0.12.3

1 year ago

0.15.1

8 months ago

0.12.4

10 months ago

0.11.0

1 year ago

0.10.3

1 year ago

0.10.0

1 year ago

0.10.1

1 year ago

0.10.2

1 year ago

0.9.1

1 year ago

0.9.0

1 year ago

0.8.1

1 year ago

0.8.0

1 year ago

0.7.0

1 year ago

0.6.7

1 year ago

0.6.5

1 year ago

0.6.4

1 year ago

0.6.2

1 year ago

0.6.1

1 year ago

0.6.0

1 year ago

0.3.0

1 year ago

0.5.0

1 year ago

0.4.0

1 year ago

0.5.2

1 year ago

0.5.1

1 year ago

0.2.0

1 year ago

0.1.0

1 year ago