425a7bc6e7
Type guards take `unknown`; walkers/comparators (get/set/isEqual) narrow via casts; generic defaults and constraints tightened. Truly-idiomatic any-function constraints and load-bearing type-level any are kept with explanatory comments.
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
import type { Collection, Path } from '../../types';
|
|
|
|
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K>
|
|
= K extends keyof O
|
|
? O[K]
|
|
: K extends keyof NonNullable<O>
|
|
? NonNullable<O>[K]
|
|
: never;
|
|
|
|
export type ExtractFromArray<A extends readonly unknown[], K>
|
|
// `any[]` (not `unknown[]`) is load-bearing: `any[] extends number[]` is true while
|
|
// `unknown[] extends number[]` is false. This distinguishes a general array (widen the
|
|
// element with `undefined`) from a fixed tuple (resolve the exact element at index K).
|
|
= any[] extends A
|
|
? A extends ReadonlyArray<infer T>
|
|
? T | undefined
|
|
: undefined
|
|
: K extends keyof A
|
|
? A[K]
|
|
: undefined;
|
|
|
|
export type ExtractFromCollection<O, K>
|
|
= K extends []
|
|
? O
|
|
: K extends [infer Key, ...infer Rest]
|
|
? O extends Record<PropertyKey, unknown>
|
|
? ExtractFromCollection<ExtractFromObject<O, Key>, Rest>
|
|
: O extends readonly unknown[]
|
|
? ExtractFromCollection<ExtractFromArray<O, Key>, Rest>
|
|
: never
|
|
: never;
|
|
|
|
export type Get<O, K> = ExtractFromCollection<O, Path<K>>;
|
|
|
|
/**
|
|
* @name get
|
|
* @category Collections
|
|
* @description Safely read a deeply nested value from a collection by a dot-separated path
|
|
*
|
|
* @param {Collection} obj - The source object or array
|
|
* @param {string} path - Dot-separated path, e.g. `'user.addresses.0.street'`
|
|
* @returns {Get<O, K> | undefined} The resolved value, or `undefined` if any segment is missing
|
|
*
|
|
* @example
|
|
* get({ user: { name: 'John' } }, 'user.name'); // 'John'
|
|
* get({ items: [{ id: 1 }] }, 'items.0.id'); // 1
|
|
* get({ a: 1 }, 'a.b.c'); // undefined
|
|
*
|
|
* @since 0.0.4
|
|
*/
|
|
export function get<O extends Collection, K extends string>(obj: O, path: K): Get<O, K> | undefined {
|
|
let value: unknown = obj;
|
|
let start = 0;
|
|
|
|
// Walk the path without allocating an intermediate array of segments.
|
|
for (let i = 0, len = path.length; i <= len; i++) {
|
|
// Split on '.' (char code 46) or the end of the string.
|
|
if (i !== len && path.charCodeAt(i) !== 46)
|
|
continue;
|
|
|
|
if (value === null || value === undefined)
|
|
return undefined;
|
|
|
|
value = (value as Record<string, unknown>)[path.slice(start, i)];
|
|
start = i + 1;
|
|
}
|
|
|
|
return value as Get<O, K> | undefined;
|
|
}
|