Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2721a28
feat(CI-CD): ✨ read type definitions from source files for accuracy
baxyz Jun 13, 2026
070ad7a
refactor(array): ♻️ mark DEFAULT_SORT_STRING_PROPS as internal
baxyz Jun 14, 2026
eb3bca5
feat(CI-C CD): ✨ add runtimes field for consumer compatibility
baxyz Jun 14, 2026
ba8e6ea
feat(type): ✨ add isInfinite example and notes to native alternatives
baxyz Jun 14, 2026
bbc458b
feat(node): ✨ add isNodeStream function and examples
baxyz Jun 14, 2026
1ae5796
feat(node): ✨ add isSharedArrayBuffer function and examples
baxyz Jun 14, 2026
704bb1b
feat(number): ✨ add isEven function with examples and tests
baxyz Jun 14, 2026
6e88792
feat(number): ✨ add isOdd function with examples and tests
baxyz Jun 14, 2026
406d9ae
feat(observable): ✨ add isObservable function and examples
baxyz Jun 14, 2026
66aa7b2
feat(type): ✨ add isArrayLike function and examples
baxyz Jun 14, 2026
23b5060
feat(type): ✨ add isAsyncGenerator function and examples
baxyz Jun 14, 2026
cf850ba
feat(type): ✨ add isAsyncGeneratorFunction with examples and tests
baxyz Jun 14, 2026
afef1e0
feat(type): ✨ add isAsyncIterable function with examples and tests
baxyz Jun 14, 2026
19887b3
feat(type): ✨ add isGenerator function and related examples and tests
baxyz Jun 14, 2026
b76f457
feat(type): ✨ add isGeneratorFunction and related examples and tests
baxyz Jun 14, 2026
cb59307
feat(type): ✨ add isPromiseLike function with examples and tests
baxyz Jun 14, 2026
dd37945
feat(type): ✨ add isPropertyKey function with examples and tests
baxyz Jun 14, 2026
d7a15fa
docs: 📝 update isEmpty helper to split by category
baxyz Jun 14, 2026
6b70a8d
feat(array): ✨ add isEmpty and isNonEmpty helpers with tests and exam…
baxyz Jun 14, 2026
0ba8807
feat(string): ✨ add isBlank function with examples and tests
baxyz Jun 14, 2026
3cd87a7
feat(string): ✨ add isNotBlank function with examples and tests
baxyz Jun 14, 2026
a277654
feat(array): ✨ add select helper with examples and tests
baxyz Jun 14, 2026
631b9ac
feat(date): ✨ add isValid function and related tests
baxyz Jun 14, 2026
569e086
feat(number): ✨ add isPositiveNumber function with tests
baxyz Jun 14, 2026
cf684d0
feat(number): ✨ add isNegative function with tests
baxyz Jun 14, 2026
f31f9ac
docs(type): 📝 update links to use date/isValid
baxyz Jun 14, 2026
d368e87
chore: 🔧 remove isNonEmptyArray and isNonEmptyString implementations …
baxyz Jun 14, 2026
b08de9a
fix(review): address code review findings
baxyz Jun 14, 2026
f553c68
fix(ci): fix lint and build failures
baxyz Jun 14, 2026
7b89dfc
fix(CI-CD): 🐛 improve comments for built-in module check
baxyz Jun 15, 2026
15cda90
chore: 🔧 update dependencies for babel packages
baxyz Jun 15, 2026
abc1cf9
feat(number): ✨ add extractNumber function and related tests
baxyz Jun 16, 2026
1efc498
feat(CI-CD): ✨ add caching and comment stripping for external depende…
baxyz Jun 16, 2026
3ff8ec2
fix(security): override markdown-it to >=14.2.0
baxyz Jun 16, 2026
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
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,25 @@ helpers/<category>/

**Coverage:** 100% lines, functions, branches, statements — no exceptions.

### Helper Placement

**Type predicates** (`is<Type>`) → `type/` category.
These answer "what *type* is this value?" and return a TypeScript type guard.
Examples: `isArray`, `isString`, `isNull`, `isPromise`.

**State predicates** → **their own category**, never `type/`.
These answer "what *state* is this value in?" and are category-specific.
Category-specific examples:

- `isEmpty` for arrays → `array/isEmpty` (not `type/isEmpty`)
- `isEmpty` for objects → `object/isEmpty`
- `isEmpty` for strings → `string/isEmpty`
- `isNonEmpty` for arrays → `array/isNonEmpty`

The distinction: a type predicate narrows the TypeScript type (`value is T`); a state predicate
checks a runtime condition within an already-known type context (e.g. an array that happens to be
empty). Mixing both in `type/` blurs the category boundary and makes callers import unrelated logic.

**Intentional cross-category duplicates:** `compact` and `equalsShallow` exist in both `array/` and `object/`. Do **NOT** merge or deduplicate them — each category is an independent npm package and cross-package imports break tree-shaking.

### License Header (required on all source files)
Expand Down
93 changes: 4 additions & 89 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,12 @@
# TODO — `helpers4/typescript`

> Last refresh: 2026-05-13.
> Last refresh: 2026-06-14.

Legend: 🔴 High priority · 🟡 Medium · 🟢 Low

---

## 1. `type/` — gap fill

**Source:** [radashi-org/discussions#46](https://github.com/orgs/radashi-org/discussions/46#discussioncomment-11736331)
— comparison table of Radashi / another lib / `@sindresorhus/is` (~90 predicates).
Radashi won't act on this. helpers4 can close the relevant gaps since the design is already
tree-shakeable, browser-safe, and one-file-per-predicate.

> Note: `isNumber(NaN) === false` is already correct in helpers4 — not affected by Radash #405.

### 🔴 Numeric — common, frequently needed

| Helper | Implementation note |
|--------|---------------------|
| `isInteger` | `Number.isInteger(value)` — distinct from `isNumber` |
| `isNaN` | `Number.isNaN(value)` — safe version (not the legacy global) |
| `isSafeInteger` | `Number.isSafeInteger(value)` |
| `isInfinite` | `value === Infinity \|\| value === -Infinity` |

### 🔴 Collections — used widely

| Helper | Implementation note |
|--------|---------------------|
| `isSet` | `value instanceof Set` |
| `isWeakMap` | `value instanceof WeakMap` |
| `isWeakSet` | `value instanceof WeakSet` |
| `isWeakRef` | `value instanceof WeakRef` |

### 🟡 Numeric — nice-to-have

| Helper | Implementation note |
|--------|---------------------|
| `isEvenInteger` | `isInteger(n) && n % 2 === 0` |
| `isOddInteger` | `isInteger(n) && n % 2 !== 0` |

### 🟡 Iteration protocol

| Helper | Implementation note |
|--------|---------------------|
| `isAsyncIterable` | `Symbol.asyncIterator in Object(value)` |
| `isGenerator` | `Object.prototype.toString` → `[object Generator]` |
| `isGeneratorFunction` | `Object.prototype.toString` → `[object GeneratorFunction]` |
| `isAsyncGenerator` | `Object.prototype.toString` → `[object AsyncGenerator]` |
| `isAsyncGeneratorFunction` | `Object.prototype.toString` → `[object AsyncGeneratorFunction]` |

### 🟡 String specializations

| Helper | Implementation note |
|--------|---------------------|
| `isEmptyString` | `value === ''` |
| `isWhitespaceString` | `isString(value) && value.trim() === ''` |

> `isNonEmptyString` already exists.

### 🟡 Object / Array specializations

| Helper | Implementation note |
|--------|---------------------|
| `isEmptyArray` | `isArray(value) && value.length === 0` |
| `isEmptyObject` | `isPlainObject(value) && Object.keys(value).length === 0` |
| `isNonEmptyObject` | `isPlainObject(value) && Object.keys(value).length > 0` |

### 🟢 General purpose

| Helper | Implementation note |
|--------|---------------------|
| `isPropertyKey` | `isString(v) \|\| isNumber(v) \|\| isSymbol(v)` → `value is PropertyKey` |
| `isPromiseLike` | `value != null && typeof (value as any).then === 'function'` (thenable) |
| `isArrayLike` | `value != null && typeof (value as any).length === 'number'` |
| `isHtmlElement` | `typeof HTMLElement !== 'undefined' && value instanceof HTMLElement` — browser-only, document it |
| `isUrlInstance` | `value instanceof URL` |

### Explicitly out of scope

- Typed arrays (`Int8Array`, `Uint8Array`, etc.) — too niche, no tree-shaking benefit
- `isNodeStream`, `isSharedArrayBuffer` — Node.js specific
- `isObservable` — handled by the `observable/` category
- `isAll` / `isAny` / global `is` / `assert` — meta-predicates, different design surface
- `isClass`, `isBoundFunction`, `isTagged`, `isDirectInstanceOf`, `isEnumCase` — reflection / meta
- `isResult` / `isResultOk` / `isResultErr` — requires a Result type not shipped by this lib

---

## 2. OpenSSF Scorecard
## 1. OpenSSF Scorecard

> Last snapshot: **6.7**. Goal: lift the score by closing the highest-impact
> checks first, while keeping CI behaviour stable.
Expand Down Expand Up @@ -147,9 +65,6 @@ After each PR, re-run Scorecard and capture the delta.

---

## 3. Suggested next steps
## 2. Suggested next steps

1. **`type/` gap fill** — tackle §1 numeric + collection predicates first (high value, small effort).
One file per predicate following the existing pattern.
2. **OpenSSF PRs C/D/E** — land in parallel, they don't conflict with the helper roadmap.
3. Open one issue per accepted helper in §1 with its source reference for traceability.
1. **OpenSSF PRs C/D/E** — land in parallel with the helper roadmap.
10 changes: 10 additions & 0 deletions docs/native-alternatives.json
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@
"example": "Number.isFinite(value)",
"notes": "Prefer Number.isFinite() over global isFinite() which coerces"
},
{
"name": "isInfinite",
"libraries": [
"sindresorhus/is"
],
"native": "value === Infinity || value === -Infinity / !Number.isFinite(value) && !Number.isNaN(value)",
"since": "ES2015",
"example": "value === Infinity || value === -Infinity\n// or: !Number.isFinite(value) && !Number.isNaN(value)",
"notes": "Number.isFinite() is the complement; compose with a NaN guard if needed. No dedicated helper warranted."
},
{
"name": "isSet (Set data structure)",
"libraries": [
Expand Down
47 changes: 47 additions & 0 deletions helpers/array/isEmpty.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: LGPL-3.0-or-later
*/

import type { HelperExamples } from '../../scripts/examples/types';
import { isEmpty } from './isEmpty';

const examples: HelperExamples = {
helper: 'isEmpty',
category: 'array',
examples: [
{
title: 'Check if an array is empty',
description: 'Returns true only for arrays with no elements.',
code: `isEmpty([]) // => true
isEmpty([1, 2, 3]) // => false
isEmpty([null]) // => false (null is still an element)`,
assert: () => {
if (!isEmpty([])) throw new Error('[] should be empty');
if (isEmpty([1, 2, 3])) throw new Error('[1,2,3] should not be empty');
if (isEmpty([null])) throw new Error('[null] should not be empty');
},
},
{
title: 'Branch on empty array with type narrowing',
description: 'In the true branch, the type narrows to never[], ensuring no element access.',
code: `function first<T>(arr: T[]): T | undefined {
if (isEmpty(arr)) return undefined;
return arr[0]; // TypeScript knows arr is non-empty here
}
first([]) // => undefined
first([1, 2]) // => 1`,
assert: () => {
function first<T>(arr: T[]): T | undefined {
if (isEmpty(arr)) return undefined;
return arr[0];
}
if (first([]) !== undefined) throw new Error('Expected undefined');
if (first([1, 2]) !== 1) throw new Error('Expected 1');
},
},
],
};

export default examples;
30 changes: 30 additions & 0 deletions helpers/array/isEmpty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: LGPL-3.0-or-later
*/

import * as fc from 'fast-check';
import { describe, expect, it } from 'vitest';
import { isEmpty } from './isEmpty';

describe('isEmpty — property-based', () => {
it('is always false for arrays with at least one element', () => {
fc.assert(
fc.property(fc.array(fc.anything(), { minLength: 1 }), (arr) => {
expect(isEmpty(arr)).toBe(false);
}),
);
});
});

describe('isEmpty — contracts', () => {
it('isEmpty and isNonEmpty are logical inverses', async () => {
const { isNonEmpty } = await import('./isNonEmpty');
fc.assert(
fc.property(fc.array(fc.anything()), (arr) => {
expect(isEmpty(arr)).toBe(!isNonEmpty(arr));
}),
);
});
});
43 changes: 43 additions & 0 deletions helpers/array/isEmpty.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: LGPL-3.0-or-later
*/

import { describe, expect, it } from 'vitest';
import { isEmpty } from './isEmpty';

describe('isEmpty', () => {
it('should return true for an empty array', () => {
expect(isEmpty([])).toBe(true);
});

it('should return false for a non-empty array', () => {
expect(isEmpty([1])).toBe(false);
});

it('should return false for an array with multiple elements', () => {
expect(isEmpty([1, 2, 3])).toBe(false);
});

it('should return false for an array containing falsy values', () => {
expect(isEmpty([null])).toBe(false);
expect(isEmpty([undefined])).toBe(false);
expect(isEmpty([0])).toBe(false);
expect(isEmpty([''])).toBe(false);
expect(isEmpty([false])).toBe(false);
});

it('should work with readonly arrays', () => {
const arr: readonly number[] = [];
expect(isEmpty(arr)).toBe(true);
});

it('should narrow type to never[] in true branch', () => {
const arr: string[] = [];
if (isEmpty(arr)) {
const _: readonly never[] = arr;
expect(_).toEqual([]);
}
});
});
18 changes: 18 additions & 0 deletions helpers/array/isEmpty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: LGPL-3.0-or-later
*/

/**
* Checks if an array is empty (has no elements).
* @param value - The array to check
* @returns `true` if the array has no elements
* @example
* isEmpty([]) // => true
* isEmpty([1, 2, 3]) // => false
* @since next
*/
export function isEmpty(value: readonly unknown[]): value is readonly never[] {
return value.length === 0;
}
47 changes: 47 additions & 0 deletions helpers/array/isNonEmpty.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: LGPL-3.0-or-later
*/

import type { HelperExamples } from '../../scripts/examples/types';
import { isNonEmpty } from './isNonEmpty';

const examples: HelperExamples = {
helper: 'isNonEmpty',
category: 'array',
examples: [
{
title: 'Check if an array has elements',
description: 'Returns true for arrays with at least one element, regardless of the element values.',
code: `isNonEmpty([1, 2, 3]) // => true
isNonEmpty([null]) // => true (null is still an element)
isNonEmpty([]) // => false`,
assert: () => {
if (!isNonEmpty([1, 2, 3])) throw new Error('[1,2,3] should be non-empty');
if (!isNonEmpty([null])) throw new Error('[null] should be non-empty');
if (isNonEmpty([])) throw new Error('[] should not be non-empty');
},
},
{
title: 'Safe first-element access with type narrowing',
description: 'In the true branch, the type narrows to [T, ...T[]], making arr[0] always defined.',
code: `function first<T>(arr: readonly T[]): T | undefined {
if (isNonEmpty(arr)) return arr[0]; // arr[0] is T, not T | undefined
return undefined;
}
first([1, 2]) // => 1
first([]) // => undefined`,
assert: () => {
function first<T>(arr: readonly T[]): T | undefined {
if (isNonEmpty(arr)) return arr[0];
return undefined;
}
if (first([1, 2]) !== 1) throw new Error('Expected 1');
if (first([]) !== undefined) throw new Error('Expected undefined');
},
},
],
};

export default examples;
35 changes: 35 additions & 0 deletions helpers/array/isNonEmpty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: LGPL-3.0-or-later
*/

import * as fc from 'fast-check';
import { describe, expect, it } from 'vitest';
import { isNonEmpty } from './isNonEmpty';

describe('isNonEmpty — property-based', () => {
it('is always true for arrays with at least one element', () => {
fc.assert(
fc.property(fc.array(fc.anything(), { minLength: 1 }), (arr) => {
expect(isNonEmpty(arr)).toBe(true);
}),
);
});

it('is always false for empty arrays', () => {
expect(isNonEmpty([])).toBe(false);
});
});

describe('isNonEmpty — contracts', () => {
it('first element is accessible without undefined when isNonEmpty is true', () => {
fc.assert(
fc.property(fc.array(fc.integer(), { minLength: 1 }), (arr) => {
if (isNonEmpty(arr)) {
expect(arr[0]).toBeDefined();
}
}),
);
});
});
Loading
Loading