utils: introduce getIn and setIn#711
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 1e7b72b The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR adds two new nested-path utilities (getIn and setIn) to @noaignite/utils, along with tests, public exports, and changesets to publish the new helpers.
Changes:
- Add
getInfor reading nested values by dot-path or array-path (with optional default). - Add
setInfor immutably writing nested values by dot-path or array-path (including array index creation). - Export both helpers from the utils entrypoint and add changesets for a minor release.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/utils/src/getIn.ts | Introduces getIn path reader and shared Path/PathKey types |
| packages/utils/src/getIn.test.ts | Adds vitest coverage for getIn |
| packages/utils/src/setIn.ts | Introduces setIn immutable nested writer |
| packages/utils/src/setIn.test.ts | Adds vitest coverage for setIn |
| packages/utils/src/index.ts | Re-exports getIn and setIn from the package entrypoint |
| .changeset/yummy-carrots-stand.md | Changeset for getIn release note |
| .changeset/dark-stars-love.md | Changeset for setIn release note |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| '@noaignite/utils': minor | ||
| --- | ||
|
|
||
| getIn: add new helper which gets nested property of objects. |
There was a problem hiding this comment.
Spelling/grammar: the changeset description is awkward and slightly misleading (the helper also works with arrays). Consider rephrasing to something like “Add getIn helper for reading nested values by path.”
| getIn: add new helper which gets nested property of objects. | |
| Add `getIn` helper for reading nested values by path. |
| '@noaignite/utils': minor | ||
| --- | ||
|
|
||
| setIn: add new helper which sets nested property of objects. |
There was a problem hiding this comment.
Spelling/grammar: the changeset description is awkward and slightly misleading (the helper also works with arrays). Consider rephrasing to something like “Add setIn helper for writing nested values by path.”
| setIn: add new helper which sets nested property of objects. | |
| Add `setIn` helper for writing nested values by path. |
| if (isLast) { | ||
| targetCursor[key] = value | ||
| return |
There was a problem hiding this comment.
setIn writes arbitrary path keys directly onto objects/arrays without blocking special keys like __proto__, constructor, or prototype. This can enable prototype pollution / unexpected prototype mutation when paths come from untrusted input. Consider explicitly rejecting these keys (or throwing) before assignment.
| function cloneShallow(value: unknown): unknown { | ||
| if (Array.isArray(value)) { | ||
| return [...value] | ||
| } | ||
|
|
||
| if (isObjectLike(value)) { | ||
| return { ...value } | ||
| } |
There was a problem hiding this comment.
cloneShallow treats any object-like value (e.g. Date, Map, class instances) as a plain object and clones it via spread, which drops prototypes and non-enumerable/internal state. This leads to data loss if setIn traverses through non-plain objects. Consider restricting cloning/traversal containers to arrays + plain objects (e.g. via existing isPlainObject), and otherwise replace with a fresh container (or document that only POJOs/arrays are supported).
| export function setIn<T>(object: T, path: Path, value: unknown): T { | ||
| const normalizedPath = normalizePath(path) | ||
|
|
||
| if (normalizedPath.length === 0) { | ||
| return value as T | ||
| } |
There was a problem hiding this comment.
The setIn<T>(object: T, ...) => T signature is unsound: when the path normalizes to empty (e.g. [] or '') it returns value cast to T, and when object is not object-like it fabricates a container and still casts to T. This can produce incorrect types for callers. Consider changing the signature/overloads so the return type reflects these cases (e.g. return unknown/T | TValue, or disallow empty paths via types/runtime checks).
| } | ||
|
|
||
| /** | ||
| * Returns a new object/array with a deeply nested value written at `path`. |
There was a problem hiding this comment.
JSDoc for setIn is much less detailed than other exported utils in this package (typically includes @param, @returns, and an @example). Please expand it so API consumers understand supported inputs (e.g. arrays vs plain objects), root-replacement behavior on empty path, and immutability guarantees.
| * Returns a new object/array with a deeply nested value written at `path`. | |
| * Returns a new object or array with a deeply nested value written at {@link path}. | |
| * | |
| * This function never mutates {@link object}. Instead, it creates shallow copies of | |
| * all containers (objects/arrays) along the path and returns a new root value that | |
| * shares unchanged subtrees with the original. | |
| * | |
| * The {@link path} can be provided either as: | |
| * - a dot-separated string (e.g. `"user.address.street"`), where segments that are | |
| * numeric (e.g. `"0"`, `"1"`) are treated as array indices, or | |
| * - a {@link Path} array of keys/indices. | |
| * | |
| * If {@link object} is not object-like (i.e. not a plain object or array), a new | |
| * container is created as needed based on the first path segment: numeric keys | |
| * create arrays, and non-numeric keys create plain objects. | |
| * | |
| * If {@link path} is an empty string, an empty array, or otherwise normalizes to an | |
| * empty path, the function does not traverse and simply returns {@link value} as the | |
| * new root value. | |
| * | |
| * @typeParam T - Type of the root value before the update. | |
| * @param object - The original root value. Typically a plain object or array. It is | |
| * never mutated. | |
| * @param path - The path at which to write {@link value}, as a dot-separated string | |
| * or {@link Path} array. Numeric segments/keys are treated as array indices. | |
| * @param value - The value to write at the given path. | |
| * @returns A new value of type {@link T} with {@link value} written at the given | |
| * path. The original {@link object} is left unchanged. When {@link path} | |
| * normalizes to empty, {@link value} itself is returned. | |
| * | |
| * @example | |
| * // Update a nested property on a plain object (immutable): | |
| * const original = { user: { name: 'Alice', tags: ['admin'] } } | |
| * const updated = setIn(original, 'user.name', 'Bob') | |
| * // original.user.name === 'Alice' | |
| * // updated.user.name === 'Bob' | |
| * | |
| * @example | |
| * // Update an array element using a numeric segment: | |
| * const original = { items: ['a', 'b', 'c'] } | |
| * const updated = setIn(original, 'items.1', 'B') | |
| * // original.items[1] === 'b' | |
| * // updated.items[1] === 'B' | |
| * | |
| * @example | |
| * // Replace the root value when the path is empty: | |
| * const original = { user: { name: 'Alice' } } | |
| * const updated = setIn(original, '', { user: { name: 'Bob' } }) | |
| * // updated === { user: { name: 'Bob' } } | |
| * // original is unchanged |
| * @param path - Dot-separated string path or array path. | ||
| * @param defaultValue - Value returned when the path does not exist. | ||
| */ | ||
| export function getIn<TDefault = undefined>(object: unknown, path: Path, defaultValue?: TDefault) { |
There was a problem hiding this comment.
getIn’s generic TDefault currently doesn’t improve the return type because the function returns unknown on success; unknown | TDefault collapses to unknown. This can mislead consumers into thinking the default value affects typing. Consider removing the generic and typing defaultValue as unknown, or redesigning the typing with overloads/typed object+path generics if you want stronger inference.
| export function getIn<TDefault = undefined>(object: unknown, path: Path, defaultValue?: TDefault) { | |
| export function getIn(object: unknown, path: Path, defaultValue?: unknown): unknown { |
| * | ||
| * @param object - Source object. | ||
| * @param path - Dot-separated string path or array path. | ||
| * @param defaultValue - Value returned when the path does not exist. |
There was a problem hiding this comment.
JSDoc for getIn is missing the @returns section and an example, which is inconsistent with most other exported utils in this package. Please add return semantics (including behavior on empty path and missing keys) and a small example.
| * @param defaultValue - Value returned when the path does not exist. | |
| * @param defaultValue - Value returned when the path does not exist. | |
| * @returns The value found at the given path. If the normalized path is empty, | |
| * the original {@link object} is returned. If any key on the path is missing | |
| * or not an own property of the current object, {@link defaultValue} is | |
| * returned (which may be `undefined` if not provided). | |
| * | |
| * @example | |
| * const state = { user: { profile: { name: 'Ada' } } } | |
| * | |
| * getIn(state, 'user.profile.name') // -> 'Ada' | |
| * getIn(state, ['user', 'missing'], 'N/A') // -> 'N/A' | |
| * getIn(state, '') // -> state |
24e1ef8 to
1e7b72b
Compare
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (82.35%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage. @@ Coverage Diff @@
## main #711 +/- ##
==========================================
+ Coverage 69.23% 69.86% +0.63%
==========================================
Files 64 66 +2
Lines 1001 1052 +51
Branches 247 266 +19
==========================================
+ Hits 693 735 +42
- Misses 248 252 +4
- Partials 60 65 +5
🚀 New features to boost your workflow:
|
getInsetIn