Skip to content

feat(guards): add @packrat/guards runtime type guard package#2038

Merged
andrew-bierman merged 1 commit into
developmentfrom
feat/guards-package
Apr 11, 2026
Merged

feat(guards): add @packrat/guards runtime type guard package#2038
andrew-bierman merged 1 commit into
developmentfrom
feat/guards-package

Conversation

@andrew-bierman
Copy link
Copy Markdown
Collaborator

Summary

New @packrat/guards workspace package providing one canonical import path for runtime type narrowing across the monorepo. Adding this so we can stop reaching into radash directly from every file, stop copying typeAssertions.ts into every app, and stop using as casts at API → store boundaries.

What's in it

radash re-exports (one import path instead of many):
isString, isNumber, isDate, isObject, isArray, isBoolean, isEmpty, isEqual, isFloat, isFunction, isInt, isPrimitive, isPromise, isSymbol.

assertions (throw on failure, narrow via asserts):
assertDefined, assertNonNull, assertPresent, assertIsString, assertIsNumber, assertIsBoolean, assertAllDefined.

narrow (return T | undefined instead of throwing — for API→store mapping):
asString, asNumber, asBoolean, asDate, asStringRecord, nullToUndefined.

enum (string literal union validation):
makeEnumGuard, assertEnum.

import { makeEnumGuard, nullToUndefined, assertDefined } from '@packrat/guards';

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 WeightUnit
}

const description = nullToUndefined(apiResponse.description); // string | null → string | undefined

Wiring

  • New private workspace at packages/guards/. Bun workspaces picks it up via packages/*.
  • tsconfig.json path entries for @packrat/guards and @packrat/guards/*.
  • radash as a direct dependency.

Not yet — follow-up

This PR is scoped to just the package. Migrating existing call sites is a separate effort:

Test plan

  • bun install picks up the new workspace
  • bun run check-types passes with a smoke-test import of every exported symbol
  • biome passes
  • CI unit/type checks pass on this PR
  • Follow-up PRs land the migrations without regression

🤖 Generated with Claude Code

Introduce a shared guards package so every app in the monorepo
has one canonical import path for runtime type narrowing instead
of reaching into radash directly, copying assertion helpers into
per-app typeAssertions.ts, or leaning on `as` casts at API → store
boundaries.

## What's in it

- **radash re-exports**: isString, isNumber, isDate, isObject,
  isArray, isBoolean, isEmpty, isEqual, isFloat, isFunction,
  isInt, isPrimitive, isPromise, isSymbol.

- **assertions** (throw on failure, narrow via `asserts`):
  assertDefined, assertNonNull, assertPresent, assertIsString,
  assertIsNumber, assertIsBoolean, assertAllDefined.

- **narrow** (return T | undefined instead of throwing):
  asString, asNumber, asBoolean, asDate, asStringRecord,
  nullToUndefined.

- **enum** (string literal union validation):
  makeEnumGuard, assertEnum.

## Wiring

- New workspace package at packages/guards (private, bun
  workspaces picks it up via packages/*).
- tsconfig paths entries for @packrat/guards and @packrat/guards/*.
- radash as a direct dependency.

## Not yet

A follow-up pass will migrate existing scattered `as` casts and
per-app typeAssertions.ts copies over to @packrat/guards. Keeping
this commit scoped to the new package so the blast radius is zero.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb4bbc1e-c309-46d8-b8a0-d8bd6dbb6847

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/guards-package

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Apr 11, 2026
@andrew-bierman andrew-bierman marked this pull request as ready for review April 11, 2026 04:31
Copilot AI review requested due to automatic review settings April 11, 2026 04:31
@andrew-bierman andrew-bierman merged commit 5ed4558 into development Apr 11, 2026
5 checks passed
@andrew-bierman andrew-bierman deleted the feat/guards-package branch April 11, 2026 04:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new internal workspace package, @packrat/guards, to centralize runtime type guards / assertions / narrowing helpers behind a single import path across the monorepo (reducing ad-hoc radash imports, local typeAssertions.ts copies, and unsafe casts).

Changes:

  • Added packages/guards workspace with assertions, enum guards, and narrowing helpers, plus radash re-exports.
  • Wired TS path aliases for @packrat/guards in the root tsconfig.json.
  • Updated bun.lock to include the new workspace (and additional dependency graph changes).

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tsconfig.json Adds path aliases for @packrat/guards and subpaths.
packages/guards/src/index.ts Public entrypoint re-exporting radash guards + local helpers.
packages/guards/src/assertions.ts New asserts-based runtime assertions.
packages/guards/src/narrow.ts New asX / narrowing helpers for API→store mapping.
packages/guards/src/enum.ts New helpers for validating string literal unions at runtime.
packages/guards/package.json Declares new private workspace package and radash dependency.
bun.lock Adds @packrat/guards workspace and updates dependency graph.

Comment on lines +10 to +24
export {
isArray,
isDate,
isEmpty,
isEqual,
isFloat,
isFunction,
isInt,
isNumber,
isObject,
isPrimitive,
isPromise,
isString,
isSymbol,
} from 'radash';
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description lists isBoolean as a radash re-export, but it is not currently exported from @packrat/guards. Either add it to this re-export list (if available in radash) or update the exported surface/docs so consumers don't hit missing import errors.

Copilot uses AI. Check for mistakes.
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>)) {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asStringRecord claims it returns {} when the input isn't a plain object, but the current check only excludes null and non-objects—arrays (and other non-plain objects) will be treated as objects and may produce unexpected keys. Tighten the guard to reject arrays / non-plain objects before iterating entries.

Suggested change
/**
* 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)) {

Copilot uses AI. Check for mistakes.
".": "./src/index.ts"
},
"dependencies": {
"radash": "^12.1.0"
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

radash is declared as ^12.1.0 here while other workspaces already depend on ^12.1.1. Consider aligning the version range to reduce lockfile churn and avoid accidental multi-version installs.

Suggested change
"radash": "^12.1.0"
"radash": "^12.1.1"

Copilot uses AI. Check for mistakes.
andrew-bierman added a commit that referenced this pull request May 14, 2026
feat(guards): add @packrat/guards runtime type guard package
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants