Skip to content

Commit 0a3dd01

Browse files
feat: getObjectValue notation support
1 parent 05351b8 commit 0a3dd01

File tree

4 files changed

+185
-19
lines changed

4 files changed

+185
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# [Versions](https://github.com/Tracktor/react-utils/releases)
22

3-
## v1.23.0
4-
- **[feat]** : add util function `getObjectValue`
3+
## v1.24.0
4+
- **[feat]** : add util function `getObjectValue` with dot notation support

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@tracktor/react-utils",
33
"description": "React data table and react data grid",
4-
"version": "1.23.0",
4+
"version": "1.24.0",
55
"private": false,
66
"license": "ISC",
77
"type": "module",

src/utils/object/getObjectValue/getObjectValue.test.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,153 @@ describe("getObjectValue", () => {
5353
expect(getObjectValue(obj, "a")).toBeUndefined();
5454
expect(getObjectValue(obj, "a", 2)).toBe(2);
5555
});
56+
57+
describe("dot notation - nested access", () => {
58+
const testObject = {
59+
config: {
60+
api: {
61+
endpoints: {
62+
auth: "/api/auth",
63+
users: "/api/users",
64+
},
65+
},
66+
},
67+
user: {
68+
metadata: undefined,
69+
preferences: null,
70+
profile: {
71+
personal: {
72+
address: {
73+
city: "Paris",
74+
coordinates: {
75+
lat: 48.8566,
76+
lng: 2.3522,
77+
},
78+
country: "France",
79+
street: "123 Main St",
80+
},
81+
age: 30,
82+
name: "John Doe",
83+
},
84+
settings: {
85+
notifications: {
86+
email: true,
87+
preferences: {
88+
marketing: false,
89+
security: {
90+
loginAlerts: true,
91+
passwordChanges: true,
92+
},
93+
updates: true,
94+
},
95+
push: false,
96+
},
97+
theme: "dark",
98+
},
99+
},
100+
},
101+
};
102+
103+
it("accesses nested properties at different depths", () => {
104+
// Level 1
105+
expect(getObjectValue(testObject, "user")).toBeDefined();
106+
expect(getObjectValue(testObject, "config")).toBeDefined();
107+
108+
// Level 2
109+
expect(getObjectValue(testObject, "user.profile")).toBeDefined();
110+
expect(getObjectValue(testObject, "config.api")).toBeDefined();
111+
112+
// Level 3
113+
expect(getObjectValue(testObject, "user.profile.settings")).toBeDefined();
114+
expect(getObjectValue(testObject, "user.profile.personal")).toBeDefined();
115+
expect(getObjectValue(testObject, "config.api.endpoints")).toBeDefined();
116+
117+
// Level 4
118+
expect(getObjectValue(testObject, "user.profile.settings.theme")).toBe("dark");
119+
expect(getObjectValue(testObject, "user.profile.personal.name")).toBe("John Doe");
120+
expect(getObjectValue(testObject, "user.profile.personal.age")).toBe(30);
121+
expect(getObjectValue(testObject, "config.api.endpoints.users")).toBe("/api/users");
122+
123+
// Level 5
124+
expect(getObjectValue(testObject, "user.profile.settings.notifications.email")).toBe(true);
125+
expect(getObjectValue(testObject, "user.profile.settings.notifications.push")).toBe(false);
126+
expect(getObjectValue(testObject, "user.profile.personal.address.street")).toBe("123 Main St");
127+
expect(getObjectValue(testObject, "user.profile.personal.address.city")).toBe("Paris");
128+
129+
// Level 6
130+
expect(getObjectValue(testObject, "user.profile.settings.notifications.preferences.marketing")).toBe(false);
131+
expect(getObjectValue(testObject, "user.profile.settings.notifications.preferences.updates")).toBe(true);
132+
expect(getObjectValue(testObject, "user.profile.personal.address.coordinates.lat")).toBe(48.8566);
133+
expect(getObjectValue(testObject, "user.profile.personal.address.coordinates.lng")).toBe(2.3522);
134+
135+
// Level 7
136+
expect(getObjectValue(testObject, "user.profile.settings.notifications.preferences.security.loginAlerts")).toBe(true);
137+
expect(getObjectValue(testObject, "user.profile.settings.notifications.preferences.security.passwordChanges")).toBe(true);
138+
});
139+
140+
it("returns undefined for non-existent nested paths", () => {
141+
expect(getObjectValue(testObject, "user.profile.settings.nonExistent")).toBeUndefined();
142+
expect(getObjectValue(testObject, "user.profile.settings.theme.color")).toBeUndefined();
143+
expect(getObjectValue(testObject, "user.nonExistent.property")).toBeUndefined();
144+
expect(getObjectValue(testObject, "config.api.endpoints.nonExistent")).toBeUndefined();
145+
expect(getObjectValue(testObject, "user.profile.personal.address.coordinates.elevation")).toBeUndefined();
146+
});
147+
148+
it("returns default values for non-existent nested paths", () => {
149+
expect(getObjectValue(testObject, "user.profile.settings.language", "en")).toBe("en");
150+
expect(getObjectValue(testObject, "user.profile.personal.phone", "not provided")).toBe("not provided");
151+
expect(getObjectValue(testObject, "config.api.timeout", 5000)).toBe(5000);
152+
expect(getObjectValue(testObject, "user.profile.personal.address.zipcode", "75001")).toBe("75001");
153+
});
154+
155+
it("handles null and undefined values in nested paths", () => {
156+
expect(getObjectValue(testObject, "user.preferences")).toBeNull();
157+
expect(getObjectValue(testObject, "user.preferences", "default")).toBe("default");
158+
expect(getObjectValue(testObject, "user.preferences.something", "fallback")).toBe("fallback");
159+
160+
expect(getObjectValue(testObject, "user.metadata")).toBeUndefined();
161+
expect(getObjectValue(testObject, "user.metadata", "default")).toBe("default");
162+
expect(getObjectValue(testObject, "user.metadata.something", "fallback")).toBe("fallback");
163+
});
164+
165+
it("handles edge cases with dot notation", () => {
166+
const edgeObject = {
167+
"": "empty key",
168+
"key.with.dots": "literal dots",
169+
normal: {
170+
"": "nested empty key",
171+
"key.with.dots": "nested literal dots",
172+
},
173+
};
174+
175+
// Empty string key
176+
expect(getObjectValue(edgeObject, "")).toBe("empty key");
177+
expect(getObjectValue(edgeObject, "normal.")).toBe("nested empty key");
178+
179+
// Keys that contain literal dots (these won't work with dot notation)
180+
expect(getObjectValue(edgeObject, "key.with.dots")).toBeUndefined();
181+
expect(getObjectValue(edgeObject, "normal.key.with.dots")).toBeUndefined();
182+
});
183+
184+
it("works with arrays using numeric indices", () => {
185+
const arrayObject = {
186+
items: [{ name: "first", value: 1 }, { name: "second", nested: { deep: "value" }, value: 2 }, "string item"],
187+
matrix: [
188+
[1, 2, 3],
189+
[4, 5, 6],
190+
],
191+
};
192+
193+
expect(getObjectValue(arrayObject, "items.0.name")).toBe("first");
194+
expect(getObjectValue(arrayObject, "items.1.value")).toBe(2);
195+
expect(getObjectValue(arrayObject, "items.1.nested.deep")).toBe("value");
196+
expect(getObjectValue(arrayObject, "items.2")).toBe("string item");
197+
expect(getObjectValue(arrayObject, "matrix.0.1")).toBe(2);
198+
expect(getObjectValue(arrayObject, "matrix.1.2")).toBe(6);
199+
200+
// Non-existent array indices
201+
expect(getObjectValue(arrayObject, "items.5", "default")).toBe("default");
202+
expect(getObjectValue(arrayObject, "matrix.0.5", "missing")).toBe("missing");
203+
});
204+
});
56205
});
Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
11
/* eslint no-redeclare: "off" */
22

3+
// Unique symbol to signal a non-existent path
4+
const PATH_NOT_FOUND = Symbol("path-not-found");
5+
36
/**
4-
* Utility function to get a value from an object by key.
5-
* @param obj
6-
* @param key
7-
* @param defaultValue
7+
* Utility function to get a value from an object by key with support for dot notation.
8+
* @param obj - The object to search in
9+
* @param key - The key or path (e.g., "car.color.black")
10+
* @param defaultValue - Default value to return if path is not found
11+
*
12+
* @example
13+
* const obj = { car: { color: { black: "dark" } } };
14+
* getObjectValue(obj, "car.color.black"); // "dark"
15+
* getObjectValue(obj, "car.color.red", "not found"); // "not found"
816
*/
917
export function getObjectValue<T>(obj: unknown, key: string, defaultValue: T): T;
1018
export function getObjectValue<T = unknown>(obj: unknown, key: string): T | undefined;
1119
export function getObjectValue<T = unknown>(obj: unknown, key: string, defaultValue?: T): T | undefined {
12-
if (obj && typeof obj === "object") {
13-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
14-
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
15-
const value = descriptor?.value;
16-
17-
if (value !== undefined && value !== null) {
18-
return value;
19-
}
20+
if (!obj || typeof obj !== "object") {
21+
return defaultValue;
22+
}
2023

21-
if (defaultValue !== undefined) {
22-
return defaultValue;
23-
}
24+
const result = key.split(".").reduce((current: unknown, prop: string): unknown => {
25+
// Early return if a path already invalid or current is not a valid object
26+
if (
27+
current === PATH_NOT_FOUND ||
28+
typeof current !== "object" ||
29+
current === null ||
30+
!Object.prototype.hasOwnProperty.call(current, prop)
31+
) {
32+
return PATH_NOT_FOUND;
2433
}
34+
35+
// Return the actual value (even if it's null, undefined, etc.)
36+
return (current as Record<string, unknown>)[prop];
37+
}, obj);
38+
39+
// Return defaultValue if a path doesn't exist OR if we have 3 arguments and the result is null/undefined
40+
if (result === PATH_NOT_FOUND || (arguments.length === 3 && (result === null || result === undefined))) {
41+
return defaultValue;
2542
}
2643

27-
return defaultValue;
44+
return result as T;
2845
}
2946

3047
export default getObjectValue;

0 commit comments

Comments
 (0)