Assertions and typeguards as primitives
(opens in a new tab) (opens in a new tab) (opens in a new tab) (opens in a new tab) (opens in a new tab) (opens in a new tab) (opens in a new tab) (opens in a new tab)
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 (opens in a new tab), GitHub Readme (opens in a new tab) or generated api doc
Introduction
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 (opens in a new tab) 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
Name | Type | Comment |
---|---|---|
isPlainObject<T?> | PlainObject | |
assertPlainObject<T?> | PlainObject |
Inspired and compatible with is-plain-obj (opens in a new tab). Check the test file (opens in a new tab)
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);
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
Name | Type | Opaque type | Comment |
---|---|---|---|
isArrayNonEmpty | unknown[] | ArrayNonEmpty | |
assertArrayNonEmpty | unknown[] | ArrayNonEmpty |
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 |
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 |
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 |
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 |
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 (opens in a new tab).
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 (opens in a new tab).
Compatibility
Level | CI | Description |
---|---|---|
Node | ✅ | CI for 18.x, 20.x & 22.x. |
Browsers | ✅ | > 95% (opens in a new tab) on 12/2023. Mins to Chrome 96+, Firefox 90+, Edge 19+, iOS 12+, Safari 12+, Opera 77+ (opens in a new tab) |
Edge | ✅ | Ensured on CI with @vercel/edge-runtime (opens in a new tab). |
Typescript | ✅ | TS 5.0+ / are-the-type-wrong (opens in a new tab) checks on CI. |
ES2022 | ✅ | Dist files checked with es-check (opens in a new tab) |
For older browsers: most frontend frameworks can transpile the library (ie: nextjs (opens in a new tab)...)
Acknowledgments
Special thanks for inspiration:
Contributors
Contributions are warmly appreciated. Have a look to the CONTRIBUTING (opens in a new tab) document.
Sponsors
If my OSS work brightens your day, let's take it to new heights together! Sponsor (opens in a new tab), coffee (opens in a new tab), or star – any gesture of support fuels my passion to improve. Thanks for being awesome! 🙏❤️
Special thanks to
JetBrains | Embie.be |
License
MIT © belgattitude (opens in a new tab) and contributors.