Skip to content

Commit 1fe4db5

Browse files
author
João Dias
committed
feat(getValue): improved type safety and documentation
1 parent 326bf89 commit 1fe4db5

File tree

3 files changed

+143
-99
lines changed

3 files changed

+143
-99
lines changed

cypress/test/functions/object.cy.ts

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ describe("omit", () => {
6868

6969
it("should return the original object if keys array is undefined", () => {
7070
const object = { a: 1, b: "2", c: 3 };
71+
// @ts-expect-error - This is a test
7172
const result = _.omit(object, undefined);
7273
expect(result).to.equal(object);
7374
});
@@ -129,6 +130,7 @@ describe("get", () => {
129130
});
130131

131132
it("should return undefined if path is null", () => {
133+
// @ts-expect-error - This is a test
132134
const result = _.get(simpleObject, null);
133135

134136
expect(result).to.be.undefined;
@@ -158,6 +160,7 @@ describe("set", () => {
158160

159161
it("should create missing index properties as arrays", () => {
160162
const object = {};
163+
// @ts-expect-error - This is a test
161164
const result = _.set(object, ["x", "0", "y", "z"], 5);
162165

163166
expect(result.x[0].y.z).to.equal(5);
@@ -174,18 +177,21 @@ describe("set", () => {
174177

175178
it("should handle null initial object", () => {
176179
const object = null;
180+
// @ts-expect-error - This is a test
177181
const result = _.set(object, "a.b.c", 4);
178182
expect(result).to.deep.equal({});
179183
});
180184

181185
it("should handle undefined initial object", () => {
182186
const object = undefined;
187+
// @ts-expect-error - This is a test
183188
const result = _.set(object, "a.b.c", 4);
184189
expect(result).to.deep.equal({});
185190
});
186191

187192
it("should handle undefined path", () => {
188193
const object = { a: 1 };
194+
// @ts-expect-error - This is a test
189195
const result = _.set(object, undefined, 2);
190196
expect(result).to.equal(object);
191197
});
@@ -204,6 +210,7 @@ describe("set", () => {
204210

205211
it("should handle empty path and undefined value", () => {
206212
const object = { a: 1 };
213+
// @ts-expect-error - This is a test
207214
const result = _.set(object, "");
208215
expect(result).to.equal(object);
209216
});
@@ -276,12 +283,14 @@ describe("pick", () => {
276283

277284
it("should handle null object", () => {
278285
const object = null;
286+
// @ts-expect-error - This is a test
279287
const picked = _.pick(object, ["a", "b"]);
280288
expect(picked).to.deep.equal({});
281289
});
282290

283291
it("should handle undefined object", () => {
284292
const object = undefined;
293+
// @ts-expect-error - This is a test
285294
const picked = _.pick(object, ["a", "b"]);
286295
expect(picked).to.deep.equal({});
287296
});
@@ -292,6 +301,7 @@ describe("pick", () => {
292301
b: 2,
293302
c: 3,
294303
};
304+
// @ts-expect-error - This is a test
295305
const picked = _.pick(object, ["a", "d"]);
296306
expect(picked).to.deep.equal({ a: 1 });
297307
});
@@ -329,21 +339,25 @@ describe("has", () => {
329339

330340
it("should handle null object", () => {
331341
const object = null;
342+
// @ts-expect-error - This is a test
332343
expect(_.has(object, "a")).to.be.false;
333344
});
334345

335346
it("should handle undefined object", () => {
336347
const object = undefined;
348+
// @ts-expect-error - This is a test
337349
expect(_.has(object, "a")).to.be.false;
338350
});
339351

340352
it("should handle null path", () => {
341353
const object = { a: { bar: 2 } };
354+
// @ts-expect-error - This is a test
342355
expect(_.has(object, null)).to.be.false;
343356
});
344357

345358
it("should handle undefined path", () => {
346359
const object = { a: { bar: 2 } };
360+
// @ts-expect-error - This is a test
347361
expect(_.has(object, undefined)).to.be.false;
348362
});
349363

@@ -399,6 +413,7 @@ describe("isEqual function", () => {
399413
});
400414

401415
it("should return false for different values", () => {
416+
// @ts-expect-error - This is a test
402417
expect(_.isEqual(42, "42")).to.be.false;
403418
expect(_.isEqual({ a: 1 }, { b: 2 })).to.be.false;
404419
expect(_.isEqual([1, 2, 3], [1, 2])).to.be.false;
@@ -436,66 +451,89 @@ describe("isEqual function", () => {
436451
});
437452

438453
describe("getValue", () => {
439-
it("should return the value corresponding to the provided path", () => {
440-
const obj = {
441-
a: {
442-
b: {
443-
c: 123,
444-
},
454+
const testObject = {
455+
a: {
456+
b: {
457+
c: 123,
445458
},
446-
};
447-
const result = _.getValue(obj, "a.b.c");
448-
expect(result).to.equal(123);
459+
},
460+
items: [
461+
{ id: 1, name: "Item 1" },
462+
{ id: 2, name: "Item 2" },
463+
],
464+
};
465+
466+
it("should return the value at the specified path", () => {
467+
expect(_.getValue(testObject, "a.b.c")).to.equal(123);
468+
expect(_.getValue(testObject, "items[0].name")).to.equal("Item 1");
449469
});
450470

451-
it("should return the default value if the path does not exist and required is false", () => {
452-
const obj = {
453-
a: {
454-
b: {
455-
c: 123,
456-
},
457-
},
458-
};
471+
it("should throw error for empty path", () => {
472+
expect(() => _.getValue(testObject, "")).to.throw(
473+
"[@feedzai/js-utilities] getValue: Path must be a non-empty string"
474+
);
475+
expect(() => _.getValue(testObject, " ")).to.throw(
476+
"[@feedzai/js-utilities] getValue: Path must be a non-empty string"
477+
);
478+
});
479+
480+
it("should throw error for non-object input", () => {
481+
expect(() => _.getValue(null, "a.b.c")).to.throw(
482+
"[@feedzai/js-utilities] getValue: Input must be a valid object."
483+
);
484+
expect(() => _.getValue(undefined, "a.b.c")).to.throw(
485+
"[@feedzai/js-utilities] getValue: Input must be a valid object."
486+
);
487+
expect(() => _.getValue(42, "a.b.c")).to.throw(
488+
"[@feedzai/js-utilities] getValue: Input must be a valid object."
489+
);
490+
});
491+
492+
it("should return default value when path doesn't exist and not required", () => {
459493
const defaultValue = "default";
460-
const result = _.getValue(obj, "a.b.d", { defaultValue, required: false });
461-
expect(result).to.equal(defaultValue);
494+
expect(_.getValue(testObject, "a.b.d", { defaultValue })).to.equal(defaultValue);
495+
expect(_.getValue(testObject, "x.y.z", { defaultValue })).to.equal(defaultValue);
462496
});
463497

464-
it("should emit a warning and return the default value if the path does not exist and required is true", () => {
465-
const obj = {
466-
a: {
467-
b: {
468-
c: 123,
469-
},
470-
},
471-
};
472-
const defaultValue = "6fe42570-8204-4fdb-8df7-7542a328b590";
498+
it("should return undefined when path doesn't exist, not required, and no default value", () => {
499+
expect(_.getValue(testObject, "a.b.d")).to.be.undefined;
500+
expect(_.getValue(testObject, "x.y.z")).to.be.undefined;
501+
});
502+
503+
it("should emit warning and return default value when path is required but doesn't exist", () => {
504+
const defaultValue = "default";
473505
const spyWarn = cy.spy(console, "warn");
474-
const result = _.getValue(obj, "a.b.d", { defaultValue, required: true });
475-
expect(result).to.equal(defaultValue);
476506

507+
const result = _.getValue(testObject, "a.b.d", { defaultValue, required: true });
508+
509+
expect(result).to.equal(defaultValue);
477510
cy.wrap(spyWarn).should(
478511
"have.been.calledOnceWithExactly",
479-
`[@feedzai/js-utilities] The path a.b.d does not exist on the object. Using ${defaultValue} instead.`
512+
"[@feedzai/js-utilities] getValue: Path a.b.d does not exist on the object. Using the default value instead."
480513
);
481514
});
482515

483-
it("should emit an error if the path does not exist and required is true and no default value is provided", () => {
484-
const obj = {
516+
it("should throw error when path is required but doesn't exist and no default value", () => {
517+
expect(() => _.getValue(testObject, "a.b.d", { required: true })).to.throw(
518+
'[@feedzai/js-utilities] getValue: The required path "a.b.d" was not found and no defaultValue was provided.'
519+
);
520+
});
521+
522+
it("should handle array indices in path", () => {
523+
expect(_.getValue(testObject, "items[1].name")).to.equal("Item 2");
524+
expect(_.getValue(testObject, "items[99].name", { defaultValue: "Not Found" })).to.equal(
525+
"Not Found"
526+
);
527+
});
528+
529+
it("should handle nested objects and arrays", () => {
530+
const complexObject = {
485531
a: {
486-
b: {
487-
c: 123,
488-
},
532+
b: [{ c: 1 }, { c: 2 }, { d: { e: 3 } }],
489533
},
490534
};
491-
const spyError = cy.spy(console, "error");
492-
_.getValue(obj, "a.b.d", { required: true });
493-
expect(spyError.calledOnce).to.be.true;
494-
expect(
495-
spyError.calledWithExactly(
496-
`[@feedzai/js-utilities] The path a.b.d does not exist on the object.`
497-
)
498-
).to.be.true;
499-
spyError.restore();
535+
536+
expect(_.getValue(complexObject, "a.b[0].c")).to.equal(1);
537+
expect(_.getValue(complexObject, "a.b[2].d.e")).to.equal(3);
500538
});
501539
});

docs/docs/functions/objects/get-value.mdx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,19 @@ without any warning.
1212
## API
1313

1414
```typescript
15-
function getValue<GenericValue, GenericReturnValue>(object: GenericValue | IGetValueObject<GenericValue>, path: string, payload?: IGetValuePayload<GenericReturnValue> | undefined): GenericReturnValue | undefined;
15+
function getValue<
16+
GenericValue,
17+
GenericResult = GenericValue extends object ? unknown : never
18+
>(object: GenericValue, path: string, options?: Options & { defaultValue?: GenericResult }): GenericReturnValue | undefined;
1619
```
1720

1821
### Usage
1922

2023
```tsx
2124
import { getValue } from '@feedzai/js-utilities';
2225

23-
const OBJ = {
24-
a: {
25-
b: {
26-
c: 123,
27-
},
28-
},
29-
};
30-
31-
const RESULT = getValue(OBJ, "a.b.d", { defaultValue: "default", required: true });
32-
// => "default"
26+
const obj = { a: { b: { c: 123 } } };
27+
getValue(obj, "a.b.c"); // => 123
28+
getValue(obj, "a.b.d", { defaultValue: "fallback" }); // => "fallback"
29+
getValue(obj, "a.b.d", { required: true }); // throws Error
3330
```

src/functions/object/get-value.ts

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,77 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
/**
32
* Please refer to the terms of the license agreement in the root of the project
43
*
54
* (c) 2024 Feedzai
65
*/
7-
import { at, isObject, isUndefined } from "..";
6+
import { get, isObject, isString, isUndefined } from "..";
87

9-
type Payload = {
8+
interface Options {
9+
/**
10+
* The default value to return if the path does not exist.
11+
*/
1012
defaultValue?: unknown;
13+
/**
14+
* Whether the path is required to exist on the object.
15+
*/
1116
required?: boolean;
12-
};
17+
}
1318

1419
/**
15-
* Gets the value corresponding to the path of an object.
16-
*
17-
* If the object is required to have that path and the path does not exist, there are 2 possible outcomes:
18-
* if a default value is provided, a warning is emitted indicating that value. If not, an error is emitted.
20+
* Gets the value at the specified path of an object with optional validation.
1921
*
20-
* If the object is not required to have that path, the default value (provided or undefined) is returned,
21-
* without any warning.
22+
* @template GenericValue - The type of the input object
23+
* @template GenericResult - The type of the expected result
2224
*
23-
* @example
25+
* @param object - The source object to query
26+
* @param path - The path to the property (e.g., "a.b.c" or "items[0].name")
27+
* @param options - Optional configuration
28+
* @param options.defaultValue - Value to return if path doesn't exist
29+
* @param options.required - If true, throws error when path doesn't exist and no defaultValue provided
2430
*
25-
* ```js
26-
* import { getValue } from '@feedzai/js-utilities';
31+
* @returns The value at the specified path, or defaultValue if path doesn't exist
32+
* @throws {Error} When path is required but doesn't exist and no defaultValue is provided
2733
*
28-
* const OBJ = {
29-
* a: {
30-
* b: {
31-
* c: 123,
32-
* },
33-
* },
34-
* };
35-
*
36-
* const RESULT = getValue(OBJ, "a.b.d", { defaultValue: "a-default-value", required: true });
37-
* // => "a-default-value"
38-
* ```
34+
* @example
35+
* const obj = { a: { b: { c: 123 } } };
36+
* getValue(obj, "a.b.c"); // => 123
37+
* getValue(obj, "a.b.d", { defaultValue: "fallback" }); // => "fallback"
38+
* getValue(obj, "a.b.d", { required: true }); // throws Error
3939
*/
40-
export function getValue<T, R = unknown>(
41-
object: T,
42-
path: string,
43-
payload?: Payload | unknown
44-
): R | undefined {
45-
const TYPED_PAYLOAD = isObject(payload) ? (payload as Payload) : undefined;
46-
const DEFAULT_VAL = TYPED_PAYLOAD?.defaultValue;
47-
const RESULT = at(object, path)[0];
48-
const IS_REQUIRED = TYPED_PAYLOAD?.required ?? false;
40+
export function getValue<
41+
GenericValue,
42+
GenericResult = GenericValue extends object ? unknown : never
43+
>(object: GenericValue, path: string, options?: Options & { defaultValue?: GenericResult }) {
44+
if (!isString(path) || path.trim() === "") {
45+
throw new Error("[@feedzai/js-utilities] getValue: Path must be a non-empty string");
46+
}
4947

50-
if (!isUndefined(RESULT)) {
51-
return RESULT as R;
48+
if (!isObject(object)) {
49+
throw new Error("[@feedzai/js-utilities] getValue: Input must be a valid object.");
5250
}
5351

54-
if (!IS_REQUIRED) {
55-
return DEFAULT_VAL as R;
52+
const { defaultValue, required = false } = options ?? {};
53+
const valueResult = get<GenericValue, GenericResult>(object, path);
54+
55+
// If the value exists, return it
56+
if (!isUndefined(valueResult)) {
57+
return valueResult as GenericResult;
5658
}
5759

58-
if (!isUndefined(DEFAULT_VAL)) {
60+
// If the value is not required, return the default value instead.
61+
// This could mean that default value can be undefined.
62+
if (!required) {
63+
return defaultValue as GenericResult;
64+
}
65+
66+
// If the value is required, emit a warning and return the default value.
67+
if (!isUndefined(defaultValue)) {
5968
console.warn(
60-
`[@feedzai/js-utilities] The path ${path} does not exist on the object. Using ${DEFAULT_VAL} instead.`
69+
`[@feedzai/js-utilities] getValue: Path ${path} does not exist on the object. Using the default value instead.`
6170
);
62-
return DEFAULT_VAL as R;
71+
return defaultValue as GenericResult;
6372
}
6473

65-
console.error(`[@feedzai/js-utilities] The path ${path} does not exist on the object.`);
66-
67-
return undefined;
74+
throw new Error(
75+
`[@feedzai/js-utilities] getValue: The required path "${path}" was not found and no defaultValue was provided.`
76+
);
6877
}

0 commit comments

Comments
 (0)