Enhancing getProp() — From Simple Utility to Type-Safe Powerhouse
In the first part of this article, we introduced the idea of refactoring utility functions with TypeScript, and how it dramatically improved the reliability and maintainability of my code. One of the key functions I revamped was getProp(), which accesses deeply nested properties in an object.
But after receiving some thoughtful feedback from Jerome, I realized there’s even more we can do! In this follow-up post, we’ll break down the improvements in a side-by-side comparison — comparing the old, untyped version of getProp() with the enhanced, type-safe version.
Let’s get into it!
getProp() — From Simple Utility to Type-Safe PowerhouseThe Old Method: Accessing Nested Properties in Plain JavaScript
In the old version of getProp(), written in vanilla JavaScript, things were simple but risky. Here’s what it looked like:
function getProp(obj, path) {
const pathArray = path.split(".");
let result = obj;
for (let i = 0; i < pathArray.length; i++) {
result = result[pathArray[i]];
if (result === undefined || result === null) {
return undefined;
}
}
return result;
}
const testObj = {
user: {
name: "John",
posts: [
{ title: "Post 1", content: "This is post 1" },
{ title: "Post 2", content: "This is post 2" },
],
},
};
const postContent = getProp(testObj, "user.posts[0].content");Key Limitations:
- No Type Safety: You can’t know the types of properties being accessed at compile time. If you try to access a non-existent property, you won’t get an error until runtime.
- Arrays are tricky: You can’t handle array indices well (
posts[0]), as arrays aren’t treated as part of the object’s keys. - Error-prone: Mistyped paths like
"user.posts[0]"or"user.posts[0].content"will silently fail without warning, potentially leading to bugs. - No Optional Chaining: If any part of the path is
nullorundefined, you’re at risk of encountering errors.
The Enhanced Method: Using TypeScript for Type Safety, Arrays, and Optional Chaining
In contrast, here’s the refactored version of getProp() using TypeScript with all the enhancements we’ve discussed so far. The added complexity pays off in terms of safety, flexibility, and clarity.
Refactored Code:
type ObjPath<TObj extends Record<string, any>> = {
[K in keyof TObj & string]: TObj[K] extends Array<any>
? `${K}[${number}]` | `${K}.${ObjPath<TObj[K][number]>}`
: TObj[K] extends Record<string, any>
? K | `${K}.${ObjPath<TObj[K]>}`
: K;
}[keyof TObj & string];
type ObjResult<TObj extends Record<string, any>, TObjPath extends ObjPath<TObj>> =
TObjPath extends `${infer K extends keyof TObj & string}[${infer Index}]`
? TObj[K][number]
: TObjPath extends `${infer K extends keyof TObj & string}.${infer Rest}`
? ObjResult<TObj[K], Rest>
: TObj[TObjPath];
function getProp<TObj extends Record<string, any>, TPath extends ObjPath<TObj>>(
obj: TObj,
path: TPath,
): ObjResult<TObj, TPath> | undefined {
const [current, ...rest] = path.split(".");
if (obj[current] == null) return undefined;
if (rest.length === 0) {
return obj[current];
}
return getProp(obj[current], rest.join("."));
}
const data = {
user: {
name: "John",
posts: [
{ title: "Post 1", content: "This is post 1" },
{ title: "Post 2", content: "This is post 2" },
],
},
};
const postContent = getProp(data, "user.posts[0].content");
// Returns "This is post 1" and TypeScript ensures it's a string!Improvements:
- Type Safety:
TypeScript ensures that we access properties correctly. The type ofpostContentis automatically inferred to bestring, based on the object structure. Mistyped paths will result in compile-time errors (e.g.,"user.posts[0]"). - Arrays Supported:
Arrays are now handled as part of the path type. You can access array indices safely usingposts[0]. TypeScript knows thatdata.user.posts[0].contentis a string, so there’s no ambiguity. - Optional Chaining:
The refactored version gracefully handlesnullorundefinedat any level of the path. If any part of the path doesn’t exist, it simply returnsundefined, instead of throwing runtime errors. - Recursive Type Inference:
Using TypeScript’s recursive types, we can infer the type of deeply nested properties. For example,"user.posts[1].content"will return the correct type (string), and TypeScript ensures you can’t accidentally access the wrong type.
Additional Improvements Suggested by the Community
Thanks to the incredible feedback from Jerome, we also improved the function to address other concerns such as:
- Caching Results for Performance:
We added a simple cache to store previously accessed paths, ensuring that repeated calls for the same path don’t waste performance resources.
const cache: Record<string, any> = {};
function getPropWithCache<TObj extends Record<string, any>, TPath extends ObjPath<TObj>>(
obj: TObj,
path: TPath,
): ObjResult<TObj, TPath> | undefined {
if (cache[path]) return cache[path];
const result = getProp(obj, path);
cache[path] = result;
return result;
}- Better Array and Object Type Handling:
The community helped extend the recursiveObjPathtype to fully support arrays and nested objects, ensuring the function can scale to any depth.
Conclusion:
The enhanced getProp() function is a great example of how TypeScript’s powerful typing system can improve even the most basic utilities. By leveraging type safety, array support, optional chaining, and even caching, we can now build more robust and performant JavaScript applications.
Get Kavindu Kokila (Kavi Castelo)’s stories in your inbox
Join Medium for free to get updates from this writer.
👉 Connect with me: GitHub | LinkedIn
Thank you for being a part of the community
Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Newsletter | Podcast | Twitch
- Start your own free AI-powered blog on Differ 🚀
- Join our content creators community on Discord 🧑🏻💻
- For more content, visit plainenglish.io + stackademic.com










