import type { Collection, Path } from '../../types'; export type ExtractFromObject, K> = K extends keyof O ? O[K] : K extends keyof NonNullable ? NonNullable[K] : never; export type ExtractFromArray // `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 ? T | undefined : undefined : K extends keyof A ? A[K] : undefined; export type ExtractFromCollection = K extends [] ? O : K extends [infer Key, ...infer Rest] ? O extends Record ? ExtractFromCollection, Rest> : O extends readonly unknown[] ? ExtractFromCollection, Rest> : never : never; export type Get = ExtractFromCollection>; /** * @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 | 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(obj: O, path: K): Get | 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)[path.slice(start, i)]; start = i + 1; } return value as Get | undefined; }