DEV Community

Cover image for I was tired of TypeScript's nested object hell, so I built ts-safe-path
Cuzeac Florin
Cuzeac Florin

Posted on

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 | `${K}.${PathKeys<T[K]>}`
          : K
        : never;
    }[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?

typescript #javascript #opensource #webdev

Top comments (3)

Collapse
 
coder_one profile image
Martin Emsky

github link not working

Collapse
 
envindavsorg profile image
Cuzeac Florin

Sorry 🥲

Now is working 😇

Collapse
 
webjose profile image
José Pablo Ramírez Vargas • Edited

EDIT

This guy rang a bell, and now I remember: dot-prop

This is essentially what dot-prop does, only with different syntax. The one advantage is the inference of the keys.

Type inference on the result seems incorrect, though. You might want to check. See this playground. See the type of t. Yours, variable x assumes it can be undefined but that's incorrect. dot-prop is doing it correctly.