I was tired of TypeScript’s nested object hell, so I built ts-safe-path
The Problem 😤
We’ve all been there. You’re working with a deeply nested object in TypeScript:
const user = await fetchUser();
const city = user?.profile?.address?.city; // 🤮
And then you realize:
- No autocompletion for paths
- Runtime errors when something is undefined
- The ?. chain is getting ridiculous
- You lose type safety with dynamic paths
The Solution ✨
I built ts-safe-path – a tiny (< 2KB) TypeScript utility that gives you:
import safePath from 'ts-safe-path';
const sp = safePath(user);
const city = sp.get('profile.address.city'); // 🎉 Full autocompletion!
Mind-blowing Features 🤯
1. Autocompletion for ALL nested paths
const data =
user:
settings:
theme: 'dark',
notifications:
email: true,
push: false
;
const sp = safePath(data);
// As you type, IDE suggests:
// 'user'
// 'user.settings'
// 'user.settings.theme'
// 'user.settings.notifications'
// 'user.settings.notifications.email'
// etc...
2. Type-safe operations
// ✅ This compiles
sp.set('user.settings.theme', 'light');
// ❌ This doesn't compile
sp.set('user.settings.theme', 123); // Error: Type 'number' is not assignable to type 'string'
// ❌ This doesn't compile either
sp.get('user.settings.invalid'); // Error: Argument of type '"user.settings.invalid"' is not assignable
3. Clean API
const sp = safePath(data);
// Get values
const theme = sp.get('user.settings.theme'); // 'dark'
// Set values (creates intermediate objects if needed!)
sp.set('user.profile.avatar.url', 'https://...');
// Check existence
if (sp.has('user.settings.notifications.email'))
// ...
// Update with a function
sp.update('user.stats.loginCount', count => (count || 0) + 1);
// Deep merge
sp.merge(
user:
settings:
language: 'fr'
);
Real-world Example: React Forms
Before ts-safe-path:
const handleChange = (field: string, value: any) => {
setFormData(prev =>
const newData = ...prev ;
// Ugly nested assignment
if (field === 'user.address.city')
newData.user =
...newData.user,
address:
...newData.user.address,
city: value
;
// ... imagine doing this for 20 fields 😱
return newData;
);
};
With ts-safe-path:
const handleChange = (field: string, value: any) =>
setFormData(prev =>
const newData = ...prev ;
safePath(newData).set(field as any, value); // ✨ One line!
return newData;
);
;
The Technical Magic 🪄
The secret sauce is TypeScript’s template literal types:
type PathKeys<T> = T extends Record<string, any>
?
[K in keyof T]: K extends string
? T[K] extends Record<string, any>
? K [keyof T]
: never;
This recursively generates all possible paths as a union type!
Performance
- Zero dependencies
- < 2KB gzipped
- No runtime overhead – it’s just property access under the hood
- Tree-shakeable
Try it out!
npm install ts-safe-path
pnpm add ts-safe-path
yarn add ts-safe-path
What’s Next?
I’m planning to add:
- Array path support (users[0].name)
- Path validation at runtime
- React hooks integration
- Vue 3 composables
If this solved a problem for you, give it a ⭐ on GitHub!
What do you think? Have you faced similar issues with nested objects in TypeScript?