Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changesets

This repository uses Changesets for versioning and changelog automation.

## Common commands

```bash
npm run changeset
npm run changeset:version
npm run changeset:publish
```

Create a changeset whenever a pull request changes the published package in a way
that should produce a new version or changelog entry.
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: 2

updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
commit-message:
prefix: chore
groups:
npm-dependencies:
patterns:
- '*'

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 3
commit-message:
prefix: chore
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
jobs:
verify:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [20, 22]

steps:
- name: Checkout repository
Expand All @@ -17,7 +21,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: ${{ matrix.node-version }}
cache: npm

- name: Install dependencies
Expand All @@ -29,5 +33,20 @@ jobs:
- name: Run lint
run: npm run lint

- name: Run typecheck
run: npm run typecheck

- name: Build package
run: npm run build

- name: Run unit tests
run: npm test

- name: Run package interop smoke test
run: npm run test:package

- name: Run coverage checks
run: npm run test:coverage

- name: Check pack contents
run: npm pack --dry-run
41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Release

on:
workflow_dispatch:

permissions:
contents: write
pull-requests: write
id-token: write

jobs:
release:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
registry-url: https://registry.npmjs.org

- name: Install dependencies
run: npm ci

- name: Create release PR or publish package
uses: changesets/action@v1
with:
version: npm run changeset:version
publish: npm run release:publish
commit: 'chore: version package'
title: 'chore: version package'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
dist/
coverage/
.tmp/
.npm-cache/
*.js.map
*.tsbuildinfo
71 changes: 64 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typescript-type-guards
# @coderrob/typescript-type-guards

<p align="center">
<img src="public/img/typescript-type-guard-logo.png" alt="typescript-type-guards logo" />
Expand All @@ -9,9 +9,11 @@ Reusable TypeScript type guards for narrowing `unknown` values in application an
## Installation

```bash
npm install typescript-type-guards
npm install @coderrob/typescript-type-guards
```

The package and its development tooling require Node.js `^20.19.0`, `^22.13.0`, or `>=24.0.0`.

## Usage

```ts
Expand All @@ -22,8 +24,9 @@ import {
isDefined,
isNonEmptyString,
isNumber,
isPlainObject,
isString,
} from 'typescript-type-guards';
} from '@coderrob/typescript-type-guards';

const values: unknown[] = ['a', 'b', 'c'];

Expand Down Expand Up @@ -58,29 +61,83 @@ const input: unknown = new User(1);
if (isUser(input) && isNumber(input.id)) {
console.log(input.id);
}

const maybeConfig: unknown = { retries: 3 };

if (isPlainObject(maybeConfig)) {
console.log(maybeConfig.retries);
}
```

## Included guards

- Primitive guards: `isString`, `isNumber`, `isBoolean`, `isBigInt`, `isSymbol`, `isNull`, `isUndefined`, `isNullish`, `isNullOrUndefined`
- Numeric guards: `isFiniteNumber`, `isInteger`, `isNaN`
- Collection guards: `isArray`, `isNonEmptyArray`, `isArrayOf`, `isNonEmptyArrayOf`, `isMap`, `isSet`
- Object-like guards: `isObject`, `isFunction`, `isError`, `isRegExp`, `isDate`, `isValidDate`, `isPromise`, `isThenable`
- Object-like guards: `isObject`, `isPlainObject`, `isFunction`, `isError`, `isRegExp`, `isDate`, `isValidDate`, `isPromise`, `isThenable`
- Utility guards: `isDefined`, `isNonEmptyString`, `createTypeGuard`, `createEnumGuard`

## Package output

The published package exposes a single public entrypoint:
The published package exposes a single public entrypoint with both ESM and CommonJS support:

```ts
import { isString } from 'typescript-type-guards';
import { isString } from '@coderrob/typescript-type-guards';
```

```js
const { isString } = require('@coderrob/typescript-type-guards');
```

Type declarations are emitted to `dist/*.d.ts` during build and are included in the published package.
Type declarations are emitted during build and included in the published package.

## Benchmarks

Run the local micro-benchmarks with:

```bash
npm run bench
```

Latest local run on Windows 11 (`10.0.26200.0`) with Node.js `v22.19.0`:

| Guard | Average latency | Average throughput |
| -------------------------------------- | --------------: | -----------------: |
| `isString` on string | `39.02 ns` | `19,608,415 ops/s` |
| `isNumber` on number | `37.68 ns` | `20,371,720 ops/s` |
| `isPlainObject` on object | `37.85 ns` | `20,250,036 ops/s` |
| `createEnumGuard` result on enum value | `39.55 ns` | `19,332,792 ops/s` |

These are indicative micro-benchmark results from a single local machine. They are useful for relative comparisons inside this repository, not as a guarantee of identical performance in other runtimes or workloads.

## Development

```bash
npm run verify
npm run test:coverage
npm run build
npm run bench
npm run changeset
npm run changeset:version
npm run changeset:publish
```

## Verification

```bash
npm run format:check
npm run lint
npm run typecheck
npm test
npm run build
npm run test:package
npm run package:quality
npm run test:coverage
```

## Releases

- `npm run changeset` creates a release note entry for a package change.
- `npm run changeset:version` applies pending changesets and updates the changelog.
- `npm run release:publish` runs the full verification stack, coverage, and publishes through Changesets.
- `.github/workflows/release.yml` is a manual `workflow_dispatch` workflow for optional release publishing.
55 changes: 55 additions & 0 deletions benchmarks/guards.bench.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Bench } from 'tinybench';

import {
createEnumGuard,
isNumber,
isPlainObject,
isString,
} from '../dist/index.mjs';

const ITERATION_COUNT = Number('10000');
const BENCH_DURATION_MS = Number('100');
const NUMERIC_VALUE = Number('42');

const isStatus = createEnumGuard(
{
Active: 'ACTIVE',
Inactive: 'INACTIVE',
},
'Status',
);

const bench = new Bench({
iterations: ITERATION_COUNT,
time: BENCH_DURATION_MS,
});

/** Benchmarks an enum guard against a positive enum value input. */
function benchmarkEnumGuard() {
isStatus('ACTIVE');
}

/** Benchmarks `isNumber` against a positive number input. */
function benchmarkNumberGuard() {
isNumber(NUMERIC_VALUE);
}

/** Benchmarks `isPlainObject` against a plain object input. */
function benchmarkPlainObjectGuard() {
isPlainObject({ answer: NUMERIC_VALUE });
}

/** Benchmarks `isString` against a positive string input. */
function benchmarkStringGuard() {
isString('guard');
}

bench
.add('isString on string', benchmarkStringGuard)
.add('isNumber on number', benchmarkNumberGuard)
.add('isPlainObject on object', benchmarkPlainObjectGuard)
.add('createEnumGuard result on enum value', benchmarkEnumGuard);

await bench.run();

console.table(bench.table());
20 changes: 12 additions & 8 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from 'eslint/config';

import zeroTolerance from '@coderrob/eslint-plugin-zero-tolerance';
import tseslint from 'typescript-eslint';

export default tseslint.config(
export default defineConfig(
{
ignores: [
'dist/**',
'node_modules/**',
'coverage/**',
'scripts/**',
'eslint.config.mjs',
'jest.config.js',
],
},
...tseslint.configs.recommended,
Expand All @@ -33,11 +35,11 @@ export default tseslint.config(
complexity: ['error', { max: 3 }],
'max-lines': [
'error',
{ max: 20, skipComments: true, skipBlankLines: true },
{ max: 25, skipComments: true, skipBlankLines: true },
],
'max-lines-per-function': [
'error',
{ max: 20, skipComments: true, skipBlankLines: true },
{ max: 25, skipComments: true, skipBlankLines: true },
],
},
},
Expand All @@ -60,12 +62,14 @@ export default tseslint.config(
},
},
{
files: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
files: [
'**/__tests__/**/*.ts',
'**/*.test.ts',
'**/*.spec.ts',
'benchmarks/**/*.mjs',
],
rules: {
'max-lines': 'off',
'max-lines-per-function': 'off',
'zero-tolerance/max-function-lines': 'off',
'zero-tolerance/no-magic-numbers': 'off',
},
},
);
7 changes: 7 additions & 0 deletions fixtures/cjs-consumer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "cjs-consumer-fixture",
"private": true,
"scripts": {
"smoke": "node smoke.mjs"
}
}
Loading
Loading