-
Notifications
You must be signed in to change notification settings - Fork 38
feat(guards): add @packrat/guards runtime type guard package #2038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "@packrat/guards", | ||
| "version": "0.0.1", | ||
| "private": true, | ||
| "type": "module", | ||
| "main": "./src/index.ts", | ||
| "types": "./src/index.ts", | ||
| "exports": { | ||
| ".": "./src/index.ts" | ||
| }, | ||
| "dependencies": { | ||
| "radash": "^12.1.0" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /** | ||
| * Assertion helpers. These throw on failure and narrow the type | ||
| * of the caller's variable via `asserts` clauses. | ||
| * | ||
| * Prefer these over non-null assertions (`!`) and `as` casts when | ||
| * you need to tell TypeScript that a value is present/valid. | ||
| */ | ||
|
|
||
| export function assertDefined<T>( | ||
| value: T | undefined, | ||
| message = 'Value must be defined', | ||
| ): asserts value is T { | ||
| if (value === undefined) throw new Error(message); | ||
| } | ||
|
|
||
| export function assertNonNull<T>( | ||
| value: T | null, | ||
| message = 'Value must be non-null', | ||
| ): asserts value is T { | ||
| if (value === null) throw new Error(message); | ||
| } | ||
|
|
||
| export function assertPresent<T>( | ||
| value: T | null | undefined, | ||
| message = 'Value must be present', | ||
| ): asserts value is T { | ||
| if (value === null || value === undefined) throw new Error(message); | ||
| } | ||
|
|
||
| export function assertIsString( | ||
| value: unknown, | ||
| message = 'Expected a string', | ||
| ): asserts value is string { | ||
| if (typeof value !== 'string') throw new Error(message); | ||
| } | ||
|
|
||
| export function assertIsNumber( | ||
| value: unknown, | ||
| message = 'Expected a number', | ||
| ): asserts value is number { | ||
| if (typeof value !== 'number' || Number.isNaN(value)) throw new Error(message); | ||
| } | ||
|
|
||
| export function assertIsBoolean( | ||
| value: unknown, | ||
| message = 'Expected a boolean', | ||
| ): asserts value is boolean { | ||
| if (typeof value !== 'boolean') throw new Error(message); | ||
| } | ||
|
|
||
| export function assertAllDefined( | ||
| values: readonly unknown[], | ||
| message = 'All values must be defined', | ||
| ): void { | ||
| for (let i = 0; i < values.length; i++) { | ||
| if (values[i] === undefined) { | ||
| throw new Error(`${message} (index ${i})`); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| * Helpers for validating string literal unions at runtime. | ||
| * | ||
| * Use these when mapping API responses (`string`) into internal | ||
| * string literal types like `type WeightUnit = 'g' | 'kg' | 'oz' | 'lb'`. | ||
| */ | ||
|
|
||
| /** | ||
| * Builds a type guard for a string literal union from its members. | ||
| * | ||
| * @example | ||
| * const WEIGHT_UNITS = ['g', 'kg', 'oz', 'lb'] as const; | ||
| * type WeightUnit = (typeof WEIGHT_UNITS)[number]; | ||
| * const isWeightUnit = makeEnumGuard(WEIGHT_UNITS); | ||
| * | ||
| * if (isWeightUnit(raw)) { | ||
| * // raw is now narrowed to WeightUnit | ||
| * } | ||
| */ | ||
| export const makeEnumGuard = | ||
| <T extends string>(members: readonly T[]) => | ||
| (value: unknown): value is T => | ||
| typeof value === 'string' && (members as readonly string[]).includes(value); | ||
|
|
||
| /** | ||
| * Asserts a string belongs to a literal union, throwing otherwise. | ||
| * Narrows the caller's variable via an `asserts` clause. | ||
| */ | ||
| export function assertEnum<T extends string>( | ||
| value: unknown, | ||
| members: readonly T[], | ||
| name = 'value', | ||
| ): asserts value is T { | ||
| if (typeof value !== 'string' || !(members as readonly string[]).includes(value)) { | ||
| throw new Error(`Invalid ${name}: expected one of ${members.join(', ')}, got ${String(value)}`); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /** | ||
| * @packrat/guards — runtime type guards and narrowing helpers. | ||
| * | ||
| * Re-exports radash's primitive guards so all narrowing goes through | ||
| * one canonical import path, and adds project-specific assertions | ||
| * on top. Import from `@packrat/guards` instead of reaching into | ||
| * `radash` or scattering per-app `typeAssertions.ts` copies. | ||
| */ | ||
|
|
||
| export { | ||
| isArray, | ||
| isDate, | ||
| isEmpty, | ||
| isEqual, | ||
| isFloat, | ||
| isFunction, | ||
| isInt, | ||
| isNumber, | ||
| isObject, | ||
| isPrimitive, | ||
| isPromise, | ||
| isString, | ||
| isSymbol, | ||
| } from 'radash'; | ||
|
Comment on lines
+10
to
+24
|
||
|
|
||
| export * from './assertions'; | ||
| export * from './enum'; | ||
| export * from './narrow'; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Narrowing helpers that return `T | undefined` instead of throwing. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Useful when mapping external data (API responses, unknown records) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * into strict internal types without `as` casts. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Returns the value if it's a string, otherwise undefined. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const asString = (value: unknown): string | undefined => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof value === 'string' ? value : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Returns the value if it's a finite number, otherwise undefined. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const asNumber = (value: unknown): number | undefined => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof value === 'number' && Number.isFinite(value) ? value : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Returns the value if it's a boolean, otherwise undefined. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const asBoolean = (value: unknown): boolean | undefined => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof value === 'boolean' ? value : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Coerces null → undefined for use with `exactOptionalPropertyTypes` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * stores that only accept `string | undefined`, not `string | null`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const nullToUndefined = <T>(value: T | null): T | undefined => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value === null ? undefined : value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns the value if it's a Date, parses it if it's a string/number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * otherwise undefined. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const asDate = (value: unknown): Date | undefined => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value instanceof Date) return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value === 'string' || typeof value === 'number') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = new Date(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Number.isNaN(parsed.getTime()) ? undefined : parsed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns a `Record<string, string>` from an unknown value, keeping only | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * string-valued entries. Returns `{}` if the input isn't a plain object. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const asStringRecord = (value: unknown): Record<string, string> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value === null || typeof value !== 'object') return {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const out: Record<string, string> = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [key, val] of Object.entries(value as Record<string, unknown>)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+47
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |
| * Returns a `Record<string, string>` from an unknown value, keeping only | |
| * string-valued entries. Returns `{}` if the input isn't a plain object. | |
| */ | |
| export const asStringRecord = (value: unknown): Record<string, string> => { | |
| if (value === null || typeof value !== 'object') return {}; | |
| const out: Record<string, string> = {}; | |
| for (const [key, val] of Object.entries(value as Record<string, unknown>)) { | |
| const isPlainObject = (value: unknown): value is Record<string, unknown> => { | |
| if (value === null || typeof value !== 'object' || Array.isArray(value)) { | |
| return false; | |
| } | |
| const prototype = Object.getPrototypeOf(value); | |
| return prototype === Object.prototype || prototype === null; | |
| }; | |
| /** | |
| * Returns a `Record<string, string>` from an unknown value, keeping only | |
| * string-valued entries. Returns `{}` if the input isn't a plain object. | |
| */ | |
| export const asStringRecord = (value: unknown): Record<string, string> => { | |
| if (!isPlainObject(value)) return {}; | |
| const out: Record<string, string> = {}; | |
| for (const [key, val] of Object.entries(value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
radashis declared as^12.1.0here while other workspaces already depend on^12.1.1. Consider aligning the version range to reduce lockfile churn and avoid accidental multi-version installs.