Skip to content

utils: introduce getIn and setIn#711

Draft
adamsoderstrom wants to merge 2 commits into
mainfrom
get-in-set-in
Draft

utils: introduce getIn and setIn#711
adamsoderstrom wants to merge 2 commits into
mainfrom
get-in-set-in

Conversation

@adamsoderstrom
Copy link
Copy Markdown
Member

  • feat(utils): introduce getIn
  • feat(utils): introduce setIn

Copilot AI review requested due to automatic review settings March 20, 2026 08:37
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Mar 20, 2026 8:43am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 20, 2026

🦋 Changeset detected

Latest commit: 1e7b72b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@noaignite/utils Minor
@noaignite/react-centra-checkout Patch
@noaignite/react-utils Patch
@noaignite/tailwind-typography Patch

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 getIn for reading nested values by dot-path or array-path (with optional default).
  • Add setIn for 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.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.”

Suggested change
getIn: add new helper which gets nested property of objects.
Add `getIn` helper for reading nested values by path.

Copilot uses AI. Check for mistakes.
'@noaignite/utils': minor
---

setIn: add new helper which sets nested property of objects.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.”

Suggested change
setIn: add new helper which sets nested property of objects.
Add `setIn` helper for writing nested values by path.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +61
if (isLast) {
targetCursor[key] = value
return
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +30
function cloneShallow(value: unknown): unknown {
if (Array.isArray(value)) {
return [...value]
}

if (isObjectLike(value)) {
return { ...value }
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +47
export function setIn<T>(object: T, path: Path, value: unknown): T {
const normalizedPath = normalizePath(path)

if (normalizedPath.length === 0) {
return value as T
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
}

/**
* Returns a new object/array with a deeply nested value written at `path`.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* 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

Copilot uses AI. Check for mistakes.
* @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) {
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
export function getIn<TDefault = undefined>(object: unknown, path: Path, defaultValue?: TDefault) {
export function getIn(object: unknown, path: Path, defaultValue?: unknown): unknown {

Copilot uses AI. Check for mistakes.
*
* @param object - Source object.
* @param path - Dot-separated string path or array path.
* @param defaultValue - Value returned when the path does not exist.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* @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

Copilot uses AI. Check for mistakes.
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 82.35294% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/utils/src/setIn.ts 84.84% 2 Missing and 3 partials ⚠️
packages/utils/src/getIn.ts 77.77% 2 Missing and 2 partials ⚠️

❌ 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     
Files with missing lines Coverage Δ
packages/utils/src/getIn.ts 77.77% <77.77%> (ø)
packages/utils/src/setIn.ts 84.84% <84.84%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adamsoderstrom adamsoderstrom removed the request for review from maeertin March 20, 2026 09:20
@adamsoderstrom adamsoderstrom marked this pull request as draft March 20, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants