@voxpelli/typed-utils
Version:
My personal (type-enabled) utils / helpers
436 lines (274 loc) • 19.5 kB
Markdown
# @voxpelli/typed-utils
My personal (type-enabled) utils / helpers
[](https://www.npmjs.com/package/@voxpelli/typed-utils)
[](https://www.npmjs.com/package/@voxpelli/typed-utils)
[](https://github.com/neostandard/neostandard)
[](https://github.com/voxpelli/types-in-js)
[](https://deepwiki.com/voxpelli/typed-utils)
[](https://mastodon.social/@voxpelli)
## Requirements
> [!NOTE]
> Check the `"engines"` field in [`package.json`](./package.json) for the definitive version requirements, as they may be updated independently of this README.
- **Node.js**: ^20.11.0 or >=22.0.0
- **TypeScript**: >=5.8
## Usage
### Simple
```javascript
import { filter } from '@voxpelli/typed-utils';
/** @type {string[]} */
const noUndefined = filter(['foo', undefined]);
```
## Helpers
### Array
#### `filter(inputArray, [valueToRemove]) => filteredArray`
Takes an array as `inputArray` and a `valueToRemove` that is a string literal, `false`, `null` or `undefined`, defaulting to `undefined` if left out.
Creates a new array with all values from `inputArray` except the one that matches `valueToRemove`, then returns that array with a type where the`valueToRemove` type has also been removed from the possible values.
Can be useful in combination with eg. a `.map()` where some items in the array has resulted in `undefined` / `null` / `false` values that one wants to have removed before processing the result further.
#### `filterWithCallback(value, callback)`
Similar to `Array.prototype.filter()` but expects the `callback` to be a function like `(value: unknown) => value is any` where the `is` is the magic sauce.
#### `isArrayOfType(value, callback)`
Similar to `Array.isArray()` but also checks that the array only contains values of type verified by the `callback` function and sets the type to be an array of that type rather than simply `any[]`. The `callback` should be a function like `(value: unknown) => value is any` and needs to have an `is` in the return type for the types to work.
#### `isStringArray(value)`
Similar to `Array.isArray()` but also checks that the array only contains values of type `string` and sets the type to `string[]` rather than `any[]`.
#### `typesafeIsArray(value)`
Alias: ~~`isUnknownArray(value)`~~ (deprecated)
Does the exact same thing as `Array.isArray()` but derives the type `unknown[]` rather than `any[]`, which improves strictness in a positive type narrowing scenario (`if (typesafeIsArray(value))`) but doesn't work as well for negative type narrowing (prefer `if (!Array.isArray(value))` then).
#### `guardedArrayIncludes(collection, searchElement)`
Type-narrowing variant of `Array.prototype.includes` that works on arrays and sets. Returns `true` if `searchElement` is strictly equal to a member of `collection`. When `true`, narrows the type of `searchElement` to the element type of the iterable (`C extends Iterable<infer U> ? U : never`). Useful when you have an `unknown` (or union) value and want to both test membership and refine its type in one step.
Example:
```js
/** @type {readonly ("red"|"green"|"blue")[]} */
const COLORS = ["red", "green", "blue"];
let input /** @type {string | number} */ = Math.random() > 0.5 ? 'red' : 42;
if (guardedArrayIncludes(COLORS, input)) {
// inside: input is now "red"|"green"|"blue"
}
```
#### `ensureArray(value)`
Converts a value to an array if it isn't already one. If `value` is already an array, returns it as-is. Otherwise, wraps `value` in a new single-element array. Useful for normalizing inputs that may be either a single item or an array of items.
Example:
```js
/** @type {string | string[]} */
const input = Math.random() > 0.5 ? 'single' : ['multiple', 'items'];
const normalized = ensureArray(input); // always string[]
```
### Assertions
#### `TypeHelpersAssertionError`
Custom error class thrown by all assertion helpers in this module. You can catch this error type to specifically handle assertion failures from these utilities.
#### `TypeHelpersAssertionEqualityError`
Custom error class thrown by equality assertions such as [`assertStrictEqual(actual, expected, [message])`](#assertstrictequalactual-expected-message).
#### `assert(condition, message)`
Throws a `TypeHelpersAssertionError` if `condition` is falsy. Used internally by other assertion helpers, but can also be used directly for custom runtime assertions.
#### `assertStrictEqual(actual, expected, [message])`
Asserts strict equality (`===`) between `actual` and `expected`.
If values differ, throws a `TypeHelpersAssertionEqualityError` with `actual` / `expected` metadata that many reporters can use for diff rendering.
#### `assertObjectWithKey(obj, key)`
Asserts that `obj` is an object and contains the property `key`. Throws an error if not.
#### `assertType(value, type, [message])`
Asserts that `value` is of the given `type` (string literal, eg. `'string'`, `'number'`, `'array'`, `'null'` – same as returned by [`explainVariable()`](#explainvariablevalue)). Throws an error if not. Optional custom error message.
Supports union types by passing an array of type names:
```javascript
assertType(value, ['string', 'number']); // narrows to string | number
assertType(value, ['string', 'boolean', 'null']); // narrows to string | boolean | null
```
#### `assertKeyWithType(obj, key, type)`
Asserts that `obj` is an object, contains the property `key`, and that `obj[key]` is of the given `type`.
#### `assertKeyWithValue(obj, key, value)`
Asserts that `obj` is an object, contains the property `key`, and that `obj[key]` is strictly equal to the given `value`.
#### `assertOptionalKeyWithType(obj, key, type)`
Asserts that `obj` is an object and either does not contain the property `key`, or if present, that `obj[key]` is `undefined` or of the given `type`.
#### `assertArrayOfLiteralType(value, type, [message])`
Asserts that `value` is an array where every element is of the given `type` (string literal, eg. `'string'`, `'number'`, `'array'`, `'null'`). Throws an error if any element fails the type check. Optional custom error message.
Supports union types by passing an array of type names:
```javascript
assertArrayOfLiteralType(value, ['string', 'number']); // narrows to Array<string | number>
```
#### `assertObjectValueType(obj, type)`
Asserts that `obj` is an object where all values are of the given `type` and all keys are strings. This is useful for validating objects used as dictionaries/maps with homogeneous value types.
Supports union types by passing an array of type names:
```javascript
assertObjectValueType(obj, 'string'); // narrows to Record<string, string>
assertObjectValueType(obj, ['string', 'number', 'boolean']); // narrows to Record<string, string | number | boolean>
```
### `is`-calls / Type Checks
#### `isObject(value)`
Returns `true` if `value` is a non-null, non-array object. Narrows the type to `Record<string, unknown>`, providing an index signature for property access patterns.
Use this when you need indexed property access after the check (e.g., `value['key']` or `'key' in value`). For exhaustiveness narrowing in type discrimination chains, use [`isType(value, 'object')`](#istypevalue-type) instead, which narrows to `object`.
```javascript
if (isObject(value)) {
// value is Record<string, unknown> — can do value['key']
}
```
#### `isObjectWithKey(obj, key)`
Returns `true` if `obj` is an object and contains the property `key`.
#### `isType(value, type)`
Returns `true` if `value` is of the given `type` (string literal, eg. `'string'`, `'number'`, `'array'`, `'null'` – same as returned by [`explainVariable()`](#explainvariablevalue)).
Supports union types by passing an array of type names:
```javascript
if (isType(value, ['string', 'number'])) {
// value is narrowed to string | number
}
```
#### `isKeyWithType(obj, key, type)`
Returns `true` if `obj` is an object, contains the property `key`, and `obj[key]` is of the given `type`.
#### `isKeyWithValue(obj, key, value)`
Returns `true` if `obj` is an object, contains the property `key`, and `obj[key]` is strictly equal to the given `value`.
#### `isOptionalKeyWithType(obj, key, type)`
Returns `true` if `obj` is an object and either does not contain the property `key`, or if present, `obj[key]` is of the given `type` or `undefined`.
#### `isArrayOfLiteralType(value, type)`
Returns `true` if `value` is an array where every element is of the given `type` (string literal, eg. `'string'`, `'number'`, `'array'`, `'null'`).
Supports union types by passing an array of type names:
```javascript
if (isArrayOfLiteralType(value, ['string', 'number'])) {
// value is narrowed to Array<string | number>
}
```
#### `isObjectValueType(obj, type)`
Returns `true` if `obj` is an object where all values are of the given `type` and all keys are strings.
Supports union types by passing an array of type names:
```javascript
if (isObjectValueType(obj, 'string')) {
// obj is narrowed to Record<string, string>
}
```
#### `isPropertyKey(value)`
Runtime guard that returns `true` when `value` is a valid `PropertyKey` (`string | number | symbol`). Used internally by `hasOwn()` helpers; exported for external guard composition.
### Getters
#### `getValueOfKeyWithType(obj, key, type)`
Returns `obj[key]` when `obj` is an object, contains the property `key`, and the value at that key is of the given `type`. Returns `undefined` otherwise.
Supports union types by passing an array of type names:
```javascript
const value = getValueOfKeyWithType({ count: 1 }, 'count', ['string', 'number']);
// value: string | number | undefined
```
Useful when you want to both validate and retrieve a typed property in one step, without first calling [`isKeyWithType()`](#iskeywithtypeobj-key-type).
### Miscellaneous
#### `explainVariable(value)`
Returns a `typeof` style explanation of a variable, with added support for eg. `null` and `array`
#### `looksLikeAnErrnoException(err)`
Returns `true` if the `err` looks like being of the `NodeJS.ErrnoException` type
### Object
#### `omit(obj, keys)`
The TypeScript utility type [`Omit<obj, keys>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys) with code that does the actual omit.
#### `pick(obj, keys)`
The TypeScript utility type [`Pick<obj, keys>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) with code that does the actual pick.
#### `typedObjectKeys(obj)`
Like [`Object.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys) but typed with `Array<keyof obj>` rather than `string[]`. When `obj` is a union this means the type will resolve to only the keys _shared_ between all objects in the union.
#### `typedObjectKeysAll(obj)`
Like [`typedObjectKeys(obj)`](#typedobjectkeysobj) but when `obj` is a union this type will resolve to _all possible keys_ within that union, not just the shared ones.
#### `hasOwn(obj, key)`
Safe wrapper + type guard around `Object.hasOwn(obj, key)` that first ensures `key` is a `PropertyKey`. Narrows `key` to the intersection of keys when `obj` is a union type (same semantics as `typedObjectKeys`). Unlike the raw `in` operator, prototype chain properties are excluded.
Example:
```js
const shape = Math.random() > 0.5 ? { kind: 'a', value: 1 } : { kind: 'b', label: 'hi' };
let k /** @type {string | number | symbol} */ = 'kind';
if (hasOwn(shape, k)) {
// k narrowed to 'kind'
console.log(shape[k]); // 'a' | 'b' (further narrowed by shape.kind checks)
}
```
#### `hasOwnAll(obj, key)`
Variant of `hasOwn()` whose key type narrows to the full union of all possible keys across union object members (like `typedObjectKeysAll`). Runtime behavior is identical to `hasOwn()`; the difference is purely at the type level. Indexed access still requires a further presence guard because not every union member has every key.
Example union refinement:
```js
/** @type {{ foo: number; bar: string } | { foo: number; baz: boolean }} */
const maybe = Math.random() ? { foo: 1, bar: 'x' } : { foo: 2, baz: true };
/** @type {string} */
const key = Math.random() ? 'bar' : 'baz';
if (hasOwnAll(maybe, key)) {
// key: 'foo' | 'bar' | 'baz'
switch (key) {
case 'baz':
// Safe indexed access after runtime membership confirmation
if (key in maybe) console.log(maybe[key]);
break;
// ... more checks
}
}
```
### Object Path
#### `getObjectValueByPath(obj, path, createIfMissing)`
Returns the object at the given path within `obj`, where `path` can be a string (dot-separated) or an array of strings. If `createIfMissing` is `true`, missing objects along the path are created. Returns `false` if a non-object is encountered, or `undefined` if the path does not exist.
#### `getStringValueByPath(obj, path)`
Returns the string value at the given path within `obj`, or `false` if the value is not a string, or `undefined` if the path does not exist. The path can be a string (dot-separated) or an array of strings.
#### `getValueByPath(obj, path)`
Returns an object `{ value }` where `value` is the value at the given path within `obj`, or `false` if a non-object is encountered, or `undefined` if the path does not exist. The path can be a string (dot-separated) or an array of strings.
### Type Utilities
#### `assertTypeIsNever(value, [message])`
Asserts that a value is of type `never`, used for exhaustive switch/conditional checks. This function ensures all cases in a discriminated union are handled. If called at runtime, it throws an error indicating an unhandled case was encountered.
Useful for compile-time exhaustiveness checking:
```js
/**
* @param {'red' | 'green' | 'blue'} color
*/
function handleColor(color) {
switch (color) {
case 'red': return '#f00';
case 'green': return '#0f0';
case 'blue': return '#00f';
default:
// TypeScript error if a color case is missing
assertTypeIsNever(color);
}
}
```
#### `noopTypeIsAssignableToBase(base, superset)`
No-op function that validates at compile-time that `Superset` type is assignable to `Base` type. Does nothing at runtime. Useful for type tests and ensuring type relationships hold.
#### `noopTypeIsEmptyObject(base, shouldHaveKeys)`
No-op function that validates at compile-time that `Base` type is an empty object. Does nothing at runtime. Useful for type tests.
#### `noopTypeIsNever(value)`
No-op function that validates at compile-time that `value` is `never`. Does nothing at runtime. Useful for type tests and exhaustive switch/conditional checks where you want a non-throwing alternative to `assertTypeIsNever()`.
### Set
#### `FrozenSet`
An immutable variant of `Set`. All mutating methods (`add()`, `delete()`, `clear()`) throw a `TypeError`, making it safe to expose as a read-only collection without risking external mutation.
Useful when you want a `Set` API for membership tests (`has()`, iteration, `size`) but want to guarantee it cannot be modified after creation.
Example:
```js
import { FrozenSet } from '@voxpelli/typed-utils';
const COLORS = new FrozenSet(['red', 'green', 'blue']);
COLORS.has('red'); // true
COLORS.size; // 3
COLORS.add('purple'); // throws TypeError: Cannot modify frozen set
```
To create one from an existing `Set`:
```js
const regular = new Set([1, 2, 3]);
const frozen = new FrozenSet(regular); // copies current contents; further changes to `regular` won't affect `frozen`
```
Notes:
* Still inherits all read-only / iteration behavior from `Set` (eg. `for...of`, spread, `keys()`, `values()`).
* Throws eagerly on mutation attempts—no silent failures.
* If you need deep immutability of nested values, freeze those separately; `FrozenSet` only prevents structural changes to the set itself.
## Migration
### From 3.x to 4.x
#### `LiteralTypes['object']` changed back from `Record<string, unknown>` to `object`
`isType(value, 'object')` and `assertType(value, 'object')` now narrow to `object` instead of `Record<string, unknown>`, reverting to the original behavior from `@voxpelli/type-helpers`. The `Record<string, unknown>` mapping was introduced for convenience (indexed property access), but it broke exhaustiveness narrowing in the false branch — TypeScript could not eliminate concrete object types like `{ kind: string }` from unions, making `assertTypeIsNever()` fail.
**If you relied on indexed property access after narrowing:**
| Old pattern | Migration |
|---|---|
| `isType(x, 'object')` then `x['key']` | Use `isObject(x)` (new), or `isObjectWithKey(x, 'key')` |
| `assertType(x, 'object')` then `x['key']` | Use `assertObject(x)` (unchanged, still `Record<string, unknown>`) |
| `isType(x, 'object') && 'key' in x` | Use `isObject(x) && 'key' in x`, or `isObjectWithKey(x, 'key')` |
**If you use exhaustiveness chains** — these now work correctly:
```javascript
function process(val: string | number | { key: string }): string {
if (isType(val, 'string')) return val;
if (isType(val, 'number')) return String(val);
if (isType(val, 'object')) return val.key;
assertTypeIsNever(val); // now works — val is never
}
```
#### `LiteralTypes['function']` changed from `() => unknown` to `(...args: any[]) => unknown`
`isType(value, 'function')` and `assertType(value, 'function')` now narrow to `(...args: any[]) => unknown` instead of `() => unknown`. This is strictly more permissive — all existing code continues to work, and functions with parameters are now correctly accepted.
#### `assertTypeIsNever` now returns `never`
`assertTypeIsNever()` now has a return type of `never` instead of `void`. TypeScript recognizes it as a terminal statement (a function that never returns), so you no longer need a return statement after it in exhaustive switch/if chains.
**`eslint-plugin-jsdoc` note:** The [`require-returns-check`](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-returns-check.md) rule does not perform cross-function control flow analysis — it cannot know that a call to `assertTypeIsNever()` will always throw and never return. If you write a function with a `@returns` JSDoc that relies on `assertTypeIsNever()` as the terminal statement instead of a `return`, the rule may report "`@returns` declaration present but return expression not available in function." Suppress with `// eslint-disable-next-line jsdoc/require-returns-check` above the function's JSDoc block, or add an explicit `return` before the call (e.g., `return assertTypeIsNever(val)`). See [gajus/eslint-plugin-jsdoc#817](https://github.com/gajus/eslint-plugin-jsdoc/issues/817) for background.
<!-- ## Used by
* [`example`](https://example.com/) – used by this one to do X and Y
-->
## Similar modules
* [`type-helpers`](https://github.com/voxpelli/type-helpers) – my personal type helpers, contains no code, just types
<!--
## See also
* [Announcement blog post](#)
* [Announcement tweet](#) -->