Skip to content
VersionSize

path

The path utility safely retrieves a nested value from an object using a dot-notation string or an array of keys. It prevents runtime errors when accessing properties of undefined or null objects and supports a customizable fallback value.

Implementation

View Source Code
ts
import { assert } from '../function/assert';
import { isArray } from '../typed/isArray';
import { isNil } from '../typed/isNil';
import { IS_OBJECT_ERROR_MSG, isObject } from '../typed/isObject';
import type { Obj } from '../types';

type PathValue<T, P extends string> = P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
    ? PathValue<T[Key], Rest>
    : undefined
  : P extends keyof T
    ? T[P]
    : undefined;

// #region PathOptions
type PathOptions = {
  throwOnMissing?: boolean;
  allowArrayIndex?: boolean;
};
// #endregion PathOptions

/**
 * Retrieves the value at a given path of the object. If the value is undefined, the default value is returned.
 *
 * @example
 * ```ts
 * const obj = { a: { b: { c: 3 } }, d: [1, 2, 3] };
 *
 * getValue(obj, 'a.b.c'); // 3
 * getValue(obj, 'a.b.d', 'default'); // 'default'
 * getValue(obj, 'd[1]', undefined, { allowArrayIndex: true }); // 2
 * getValue(obj, 'e.f.g', 'default', { throwOnMissing: true }); // throws Error
 * ```
 *
 * @template T - The type of the object to query.
 * @template P - The type of the path string.
 * @param item - The object to query.
 * @param path - The path of the property to get.
 * @param [defaultValue] - The value returned for undefined resolved values.
 * @param [options] - Additional options for value retrieval.
 *
 * @returns The resolved value.
 *
 * @throws If throwOnMissing is true and the path doesn't exist.
 */
export function path<T extends Obj, P extends string>(
  item: T,
  path: P,
  defaultValue?: unknown,
  options: PathOptions = {},
): PathValue<T, P> | undefined {
  assert(isObject(item), IS_OBJECT_ERROR_MSG, { args: { item }, type: TypeError });

  const { throwOnMissing = false, allowArrayIndex = false } = options;

  const fragments = path.split(/[.[\]]+/).filter(Boolean);
  // biome-ignore lint/suspicious/noExplicitAny: -
  let current: any = item;

  for (const fragment of fragments) {
    if (isNil(current) || typeof current !== 'object') {
      return handleError(`Cannot read property '${fragment}' of ${current}`, throwOnMissing, defaultValue);
    }

    current =
      allowArrayIndex && isArray(current) && /^\d+$/.test(fragment) ? current[Number(fragment)] : current[fragment];

    if (current === undefined) {
      return handleError(`Property '${fragment}' does not exist`, throwOnMissing, defaultValue);
    }
  }

  return current as PathValue<T, P>;
}

function handleError<T extends Obj, P extends string>(
  message: string,
  throwOnMissing: boolean,
  defaultValue?: unknown,
): PathValue<T, P> | undefined {
  if (throwOnMissing) throw new Error(message);
  return defaultValue as PathValue<T, P>;
}

Features

  • Isomorphic: Works in both Browser and Node.js.
  • Safe Access: Never throws if a parent property is missing.
  • Flexible Path Formats: Use dot-notation ('a.b.c') or an array of keys (['a', 'b', 'c']).
  • Default Value Support: Provide a fallback value for missing paths.
  • Type-safe: Supports generic return types for better autocompletion.

API

Type Definitions
ts
type PathOptions = {
  throwOnMissing?: boolean;
  allowArrayIndex?: boolean;
};
ts
function path<T = any>(obj: any, path: string, fallback?: T, options?: PathOptions): T | undefined;

Parameters

  • obj: The object to query.
  • path: The path to the desired property as a dot-separated string.
  • fallback: Optional. A value to return if the path does not exist or resolves to undefined.
  • options: Optional configuration:
    • throwOnMissing: If true, throws an error instead of returning the fallback value (defaults to false).
    • allowArrayIndex: If true, supports bracket notation for array indices (e.g., 'd[1]') (defaults to false).

Returns

  • The value at the specified path, or the fallback value, or undefined.

Examples

Basic Nested Access

ts
import { path } from '@vielzeug/toolkit';

const config = {
  api: {
    endpoints: {
      login: '/api/v1/login',
    },
  },
};

path(config, 'api.endpoints.login'); // '/api/v1/login'
path(config, 'api.version'); // undefined
path(config, 'api.version', 'v1'); // 'v1'

Using Array Index with allowArrayIndex

ts
import { path } from '@vielzeug/toolkit';

const data = {
  users: [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ],
};

// Access array index with bracket notation
path(data, 'd[1]', undefined, { allowArrayIndex: true }); // 2
path(data, 'users[0].name', undefined, { allowArrayIndex: true }); // 'Alice'

Throwing on Missing Paths

ts
import { path } from '@vielzeug/toolkit';

const obj = { a: { b: 1 } };

// Throws an error instead of returning fallback
path(obj, 'e.f.g', 'default', { throwOnMissing: true }); // throws Error

Implementation Notes

  • Performance-optimized for deep traversal.
  • Correctly handles numeric keys for array indexing within paths.
  • Returns the fallback value if the resolved value is strictly undefined.

See Also

  • seek: Find a value anywhere in an object by key.
  • merge: Combine objects.
  • clone: Create a copy of an object.