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; // 🤮
Enter fullscreen mode

Exit fullscreen mode

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!
Enter fullscreen mode

Exit fullscreen mode

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...
Enter fullscreen mode

Exit fullscreen mode

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
Enter fullscreen mode

Exit fullscreen mode

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'
    
  
);
Enter fullscreen mode

Exit fullscreen mode

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;
  );
};
Enter fullscreen mode

Exit fullscreen mode

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;
  );
;
Enter fullscreen mode

Exit fullscreen mode

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;
Enter fullscreen mode

Exit fullscreen mode

This recursively generates all possible paths as a union type!

Performance

  1. Zero dependencies
  2. < 2KB gzipped
  3. No runtime overhead – it’s just property access under the hood
  4. Tree-shakeable

Try it out!

npm install ts-safe-path

pnpm add ts-safe-path

yarn add ts-safe-path
Enter fullscreen mode

Exit fullscreen mode

Check it on GitHub !

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?



Source link