|
6 | 6 |
|
7 | 7 | export * from 'klona/full'; |
8 | 8 |
|
| 9 | +/** |
| 10 | + * Asserts that a value is an object, not null, and not an array. |
| 11 | + * |
| 12 | + * Unlike {@link isObject}, this function does **not** narrow the value to a generic indexable structure. Instead, it |
| 13 | + * preserves the **existing** static type of the variable. This makes it ideal for validating option objects or |
| 14 | + * interface-based inputs where all properties may be optional. |
| 15 | + * |
| 16 | + * Use this function when: |
| 17 | + * ``` |
| 18 | + * - You expect a value to be an object at runtime, **and** |
| 19 | + * - You want to keep its compile-time type intact after validation. |
| 20 | + * ``` |
| 21 | + * |
| 22 | + * @example |
| 23 | + * interface Options { flag?: boolean; value?: number; } |
| 24 | + * |
| 25 | + * function run(opts: Options = {}) { |
| 26 | + * assertObject(opts, `'opts' is not an object.`); // `opts` remains `Options`, not widened or reduced. |
| 27 | + * opts.value; // Fully typed access remains available. |
| 28 | + * } |
| 29 | + * |
| 30 | + * @throws {TypeError} if the value is null, non-object, or an array. |
| 31 | + * |
| 32 | + * @param value - The value to validate. |
| 33 | + * |
| 34 | + * @param errorMsg - Optional message used for the thrown TypeError. |
| 35 | + */ |
| 36 | +export function assertObject(value: unknown, errorMsg: string = 'Expected an object.'): asserts value is object |
| 37 | +{ |
| 38 | + if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new TypeError(errorMsg); } |
| 39 | +} |
| 40 | + |
9 | 41 | /** |
10 | 42 | * Freezes all entries traversed that are objects including entries in arrays. |
11 | 43 | * |
@@ -484,35 +516,121 @@ export function isIterable<T>(value: unknown): value is Iterable<T> |
484 | 516 | return value !== null && typeof value === 'object' && typeof (value as any)[Symbol.iterator] === 'function'; |
485 | 517 | } |
486 | 518 |
|
| 519 | +export function isObject<T extends object>(value: T): value is T; |
| 520 | + |
487 | 521 | /** |
488 | | - * Tests for whether object is not null, typeof object, and not an array. |
| 522 | + * Runtime check for whether a value is an object: |
| 523 | + * ``` |
| 524 | + * - typeof === 'object' |
| 525 | + * - not null |
| 526 | + * - not an array |
| 527 | + * ``` |
489 | 528 | * |
490 | | - * @param value - Any value. |
| 529 | + * This function performs **type narrowing**. If the check succeeds, TypeScript refines the type of `value` to `T`, |
| 530 | + * allowing known object types (interfaces, classes, mapped structures) to retain their original shape. |
| 531 | + * |
| 532 | + * Type Behavior: |
| 533 | + * - When called with a value that already has a specific object type (interface or shaped object), that type is |
| 534 | + * preserved after narrowing. Property access remains fully typed. |
| 535 | + * |
| 536 | + * - When called with `unknown`, `any`, or an untyped object literal, `T` becomes `object`, ensuring only that a |
| 537 | + * non-null object exists. No indexing or deep property inference is provided in this case. |
| 538 | + * |
| 539 | + * In other words: |
| 540 | + * ``` |
| 541 | + * - Known object type → remains that type (preferred behavior) |
| 542 | + * - Unknown / untyped → narrows only to `object` |
| 543 | + * ``` |
491 | 544 | * |
492 | | - * @returns Is it an object. |
| 545 | + * Use this when you want runtime object validation **and** want to preserve typing when a value is already known to be |
| 546 | + * a specific object type. If you instead need to **retain** the declared type regardless of narrowing, use |
| 547 | + * {@link assertObject}. If you need indexable key / value access use a dedicated record check such as |
| 548 | + * {@link isRecord} or {@link isPlainObject}. |
| 549 | + * |
| 550 | + * @param value - Any value to check. |
| 551 | + * |
| 552 | + * @returns True if the value is a non-null object and not an array. |
493 | 553 | */ |
494 | | -export function isObject<T extends object>(value: T | unknown): value is T |
| 554 | +export function isObject(value: unknown): value is object; |
| 555 | +export function isObject(value: unknown): value is object |
495 | 556 | { |
496 | 557 | return value !== null && typeof value === 'object' && !Array.isArray(value); |
497 | 558 | } |
498 | 559 |
|
| 560 | +export function isPlainObject<T extends object>(value: T): value is T; |
| 561 | + |
499 | 562 | /** |
500 | | - * Tests for whether the given value is a plain object. |
| 563 | + * Determines whether a value is a **plain** object. |
| 564 | + * |
| 565 | + * A plain object is one whose prototype is either: |
| 566 | + * - `Object.prototype` (created via `{}` or `new Object()`) |
| 567 | + * - `null` (created via `Object.create(null)`) |
| 568 | + * |
| 569 | + * This excludes arrays, functions, class instances, DOM objects, and any object with a custom prototype. In other |
| 570 | + * words, this function detects JSON-like dictionary objects rather than structural or callable object types. |
| 571 | + * |
| 572 | + * Type Behavior: |
| 573 | + * - If the input already has a known object type `T`, that type is preserved after narrowing. |
| 574 | + * - If the input is `unknown` or untyped the result narrows to `Record<string, unknown>` allowing safe keyed access. |
501 | 575 | * |
502 | | - * An object is plain if it is created by either: `{}`, `new Object()` or `Object.create(null)`. |
| 576 | + * Useful when validating configuration objects, cloning or merging data, performing deep equality, or working with |
| 577 | + * structured JSON where non-plain / prototype values would be considered invalid. |
503 | 578 | * |
504 | | - * @param value - Any value |
| 579 | + * @example |
| 580 | + * const a = { x: 1 }; |
| 581 | + * isPlainObject(a); // true |
| 582 | + * |
| 583 | + * class Foo {} |
| 584 | + * isPlainObject(new Foo()); // false |
| 585 | + * |
| 586 | + * @example |
| 587 | + * let data: unknown = getValue(); |
| 588 | + * if (isPlainObject(data)) { |
| 589 | + * data.foo; // ok — key is `unknown`, but structure is guaranteed. |
| 590 | + * } |
| 591 | + * |
| 592 | + * @param value - Any value to evaluate. |
505 | 593 | * |
506 | | - * @returns Is it a plain object. |
| 594 | + * @returns True if the value is a plain object with no special prototype. |
507 | 595 | */ |
508 | | -export function isPlainObject<T extends object>(value: unknown): value is T |
| 596 | +export function isPlainObject(value: unknown): value is Record<string, unknown>; |
| 597 | +export function isPlainObject(value: unknown): value is Record<string, unknown> |
509 | 598 | { |
510 | 599 | if (Object.prototype.toString.call(value) !== '[object Object]') { return false; } |
511 | 600 |
|
512 | 601 | const prototype: any = Object.getPrototypeOf(value); |
513 | 602 | return prototype === null || prototype === Object.prototype; |
514 | 603 | } |
515 | 604 |
|
| 605 | +/** |
| 606 | + * Checks whether a value is a generic key / value object / `Record<string, unknown>`. |
| 607 | + * |
| 608 | + * A record in this context means: |
| 609 | + * - `typeof value === 'object'` |
| 610 | + * - value is not `null` |
| 611 | + * - value is not an array |
| 612 | + * |
| 613 | + * Unlike {@link isObject}, this function does **not** attempt to preserve the original object type. All successful |
| 614 | + * results narrow to `Record<string, unknown>` making the returned value safe for key-indexed access but without any |
| 615 | + * knowledge of property names or expected value types. |
| 616 | + * |
| 617 | + * This is useful when processing untyped JSON-like data structures, dynamic configuration blocks, response bodies, |
| 618 | + * or any case where a dictionary-style object is expected rather than a typed interface value. |
| 619 | + * |
| 620 | + * Contrast With: |
| 621 | + * - {@link isObject} → preserves known object types where possible; use when typing should remain intact. |
| 622 | + * - {@link isPlainObject} → narrows to plain JSON objects only (no prototypes, no class instances). |
| 623 | + * - `isRecord()` → always narrows to a dictionary-style record for keyed lookup. |
| 624 | + * |
| 625 | + * @param value - Any value to test. |
| 626 | + * |
| 627 | + * @returns True if the value is an object that is neither null nor an array. |
| 628 | + */ |
| 629 | +export function isRecord(value: unknown): value is Record<string, unknown> |
| 630 | +{ |
| 631 | + return value !== null && typeof value === 'object' && !Array.isArray(value); |
| 632 | +} |
| 633 | + |
516 | 634 | /** |
517 | 635 | * Safely returns keys on an object or an empty array if not an object. |
518 | 636 | * |
|
0 commit comments