Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(dset): Add merge function typing and add JSDoc documentation for dset and dset/merge #45

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 41 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
export function dset<T extends object, V>(obj: T, keys: string | ArrayLike<string | number>, value: V): void;
/**
* Dynamically sets a value within an object at a specified path, creating nested structures as necessary.
*
* This function allows for setting deeply nested values within an object by specifying a path to the target
* location. The path can be a dot-separated string (e.g., `user.address.street`) or an array of strings/numbers
* indicating the keys/indexes to traverse. Intermediate objects or arrays are created as needed based on the path.
*
* Security measures are in place to prevent prototype pollution; if the path includes `__proto__`, `constructor`,
* or `prototype`, the function will halt without making any changes.
*
* Unlike a deep merge operation, `dset` directly sets the value at the target path, overwriting any existing value.
*
* @param obj - The target object to modify.
* @param keys - The path to the target location within `obj`. Can be a dot-separated string or an array of string/number segments.
* @param val - The value to set at the target location specified by `keys`.
*
* @example
* // Using string path:
* const userProfile = {};
* dset(userProfile, 'name.first', 'Alice');
* // userProfile => { name: { first: 'Alice' } }
*
* @example
* // Using array path:
* const userScores = {};
* dset(userScores, ['scores', 'math'], 95);
* // userScores => { scores: { math: 95 } }
*
* @example
* // Attempting to set a value using a prohibited key:
* const secureObj = {};
* dset(secureObj, 'constructor.prototype.bad', 'Oops');
* // secureObj => {} (operation halted for security)
*
* @example
* // Overwriting existing values:
* const userData = { contact: { email: 'alice@example.com' } };
* dset(userData, 'contact.phone', '123-456-7890');
* // userData => { contact: { email: 'alice@example.com', phone: '123-456-7890' } }
*/
export function dset<T extends object, V>(obj: T, keys: string | ArrayLike<string | number>, val: V): void;
104 changes: 102 additions & 2 deletions src/merge.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,102 @@
export function merge(foo: any, bar: any): any; // TODO
export function dset<T extends object, V>(obj: T, keys: string | ArrayLike<string | number>, value: V): void;
type Primitive = string | number | boolean | bigint | symbol | undefined | null;
type DeepMerge<T, U> =
[T, U] extends [Primitive, Primitive] ? U :
T extends Primitive ? U :
U extends Primitive ? U :
T extends Array<unknown> ? U :
U extends Array<unknown> ? U :
{ [K in keyof T | keyof U]: K extends keyof U ? DeepMerge<T[K], U[K]> : K extends keyof T ? T[K] : never };

/**
* Deeply merges objects `a` and `b`, modifying `a` in-place.
*
* Note: This function modifies the first object (`a`) directly, incorporating properties from object `b`.
* For arrays within the objects, elements are merged index-by-index, with extra elements in the first array (`a`) preserved.
* Primitive values in `b` will overwrite those in `a` where they share the same key.
*
* @param a - The target object to be merged into and modified.
* @param b - The source object, whose properties will be merged into `a`.
* @returns The modified first object (`a`) after merging.
*
* @example
* // Basic object merging:
* const objA = { name: 'Alice', contact: { email: 'alice@example.com' } };
* const objB = { age: 30, contact: { phone: '123-456-7890' } };
* merge(objA, objB);
* // objA => { name: 'Alice', age: 30, contact: { email: 'alice@example.com', phone: '123-456-7890' } }
*
* @example
* // Array merging within objects:
* const objC = { hobbies: ['reading'] };
* const objD = { hobbies: ['cycling', 'hiking'] };
* merge(objC, objD);
* // objC => { hobbies: ['cycling', 'hiking'] }
*
* @example
* // Merging with primitive value overwrite:
* const objE = { isEnabled: false, details: { flag: true } };
* const objF = { isEnabled: true, details: { flag: false } };
* merge(objE, objF);
* // objE => { isEnabled: true, details: { flag: false } }
*
* @example
* // Deep merging with nested objects:
* const objG = { user: { name: 'Alice', address: { city: 'Wonderland' } } };
* const objH = { user: { age: 30, address: { country: 'Fantasy' } } };
* merge(objG, objH);
* // objG => { user: { name: 'Alice', age: 30, address: { city: 'Wonderland', country: 'Fantasy' } } }
*
* @example
* // Preserving extra elements in the first array:
* const objI = { values: [1, 2, 3, 4] };
* const objJ = { values: [5, 6] };
* merge(objI, objJ);
* // objI => { values: [5, 6, 3, 4] }
*/
export function merge<T extends object, U extends object>(a: T, b: U): DeepMerge<T, U>;

/**
* Sets a value within a nested object structure based on a given path.
*
* The path to the target location where the value should be set can be specified
* as a dot-separated string (e.g., `a.b.c`) or as an array of strings and/or numbers
* (e.g., `['a', 'b', 'c']`). If intermediate objects or arrays do not exist at any point
* in the path, they are created. The type of structure created (object or array) depends
* on the subsequent path segment's type or content.
*
* Security measures are in place to prevent prototype pollution; if the path includes `__proto__`, `constructor`,
* or `prototype`, the function will halt without making any changes.
*
* If a value already exists at the target path, it is deeply merged with the provided value
* using the `merge` function.
*
* @param obj - The target object to set the value in.
* @param keys - The path to the target location, specified as a dot-separated string
* or an array of string and number segments.
* @param value - The value to set at the target location.
*
* @example
* // Setting a primitive value:
* const obj1 = {};
* dset(obj1, 'a.b.c', 123);
* // obj1 => { a: { b: { c: 123 } } }
*
* @example
* // Setting a value with array notation
* const obj2 = {};
* dset(obj2, ['a', 0, 'c'], 'hello');
* // obj2 => { a: [{ c: 'hello' }] }
*
* @example
* // Attempting to set a value using a prohibited key:
* const obj3 = {};
* dset(obj3, 'constructor.prototype.bad', 'Oops');
* // obj3 => {} (operation halted for security)
*
* @example
* // Deep merging of objects
* const obj4 = { a: { b: { existing: 456 } } };
* dset(obj4, 'a.b', { new: 123 });
* // obj4 => { a: { b: { existing: 456, new: 123 } } }
*/
export function dset<T extends object, V>(obj: T, keys: string | ArrayLike<string | number>, val: V): void;