DEV Community

Cover image for Why I use Typetify: A Type-Safe Alternative to Lodash
Siddick FOFANA
Siddick FOFANA

Posted on

Why I use Typetify: A Type-Safe Alternative to Lodash

Runtime TypeScript helpers that actually protect you when it matters most.

Why I Built Typetify: A Type-Safe Alternative to Lodash

TL;DR: I built Typetify — a zero-dependency utility library for TypeScript that provides runtime safety, not just compile-time types. Think Lodash, but TypeScript-first.

npm install typetify
Enter fullscreen mode Exit fullscreen mode

The Problem with Lodash + TypeScript

Lodash is great. I've used it for years. But when you add TypeScript to the mix, something feels... off.

Example: The _.pick() Trap

import _ from 'lodash'

const user = {
  id: 1,
  name: 'John',
  email: 'john@example.com',
  password: 'secret123'
}

// TypeScript says this is fine 🤷
const safe = _.pick(user, ['id', 'name', 'emial']) // Typo!
//                                        ^^^^^^

// Runtime: { id: 1, name: 'John', emial: undefined }
// No error, no warning, just silent failure
Enter fullscreen mode Exit fullscreen mode

TypeScript can't catch this because Lodash's types are too permissive. The keys are typed as string[], not the actual keys of the object.

Example: The _.isString() Trap

function processValue(value: unknown) {
  if (_.isString(value)) {
    return value.toUpperCase()
    //     ^^^^^ TS Error: Object is of type 'unknown'
  }
}
Enter fullscreen mode Exit fullscreen mode

Lodash's type guards don't narrow types properly. You get runtime safety, but TypeScript still doesn't trust the check.


The Solution: TypeScript-First Design

Typetify solves these issues by being designed for TypeScript, not retrofitted to it.

Type-Safe pick()

import { pick } from 'typetify/object'

const user = {
  id: 1,
  name: 'John',
  email: 'john@example.com',
  password: 'secret123'
}

const safe = pick(user, ['id', 'name'])
// Type: { id: number; name: string }

// This won't compile:
const invalid = pick(user, ['id', 'emial'])
//                                ^^^^^^^ TS Error: 'emial' is not a key of user
Enter fullscreen mode Exit fullscreen mode

The keys are actually type-checked. No typos, no silent failures.

Proper Type Narrowing

import { isString } from 'typetify/guards'

function processValue(value: unknown) {
  if (isString(value)) {
    return value.toUpperCase() // ✅ TypeScript knows it's a string
  }
}
Enter fullscreen mode Exit fullscreen mode

Type guards that actually work with TypeScript's control flow analysis.


5 Real-World Use Cases

1. API Response Validation

import { hasKeys, awaitTo } from 'typetify'

async function fetchUser(id: string) {
  const [error, response] = await awaitTo(fetch(`/api/users/${id}`))
  if (error) return { error: 'Network error' }

  const data = await response.json()

  // Runtime validation with type safety
  if (!hasKeys(data, ['id', 'name', 'email'])) {
    return { error: 'Invalid response' }
  }

  // TypeScript now knows data has these keys
  return { data }
}
Enter fullscreen mode Exit fullscreen mode

2. Error Handling Without try/catch

import { awaitTo, retry, withTimeout } from 'typetify/async'

async function fetchWithRetry(url: string) {
  const [error, data] = await awaitTo(
    retry(
      () => withTimeout(fetch(url), 5000),
      { attempts: 3, delay: 1000 }
    )
  )

  if (error) {
    console.error('Failed after retries:', error)
    return null
  }

  return data
}
Enter fullscreen mode Exit fullscreen mode

No more nested try/catch blocks. Clean, readable error handling.

3. Form Data Processing

import { parseNumber, parseBoolean, compact, defaults } from 'typetify/input'

function processFormData(formData: FormData) {
  return {
    age: parseNumber(formData.get('age')), // number | undefined
    newsletter: parseBoolean(formData.get('newsletter')), // boolean
    tags: compact(formData.getAll('tags')), // string[]
    bio: defaults(formData.get('bio'), 'No bio provided'), // string
  }
}
Enter fullscreen mode Exit fullscreen mode

Parse external data safely with proper TypeScript types.

4. Filtering Arrays

import { isDefined } from 'typetify/core'

// Problem: TypeScript doesn't narrow this
const items = [1, null, 2, undefined, 3]
const numbers = items.filter(x => x != null) // (number | null | undefined)[]

// Solution: Proper type guard
const numbers = items.filter(isDefined) // number[] ✅
Enter fullscreen mode Exit fullscreen mode

5. Safe JSON Parsing

import { safeJsonParse } from 'typetify/input'

const result = safeJsonParse(jsonString)

if (result.ok) {
  console.log(result.data.name) // Type-safe access
} else {
  console.error(result.error) // Handle error
}

// No try/catch needed
Enter fullscreen mode Exit fullscreen mode

The Design Philosophy

1. Runtime First

Types are great, but they disappear at runtime. Typetify gives you both.

import { assert } from 'typetify/core'

function getUser(id: string) {
  const user = findUser(id) // User | null

  assert(user, `User ${id} not found`)
  // TypeScript now knows user is User (not null)

  return user.name // Safe!
}
Enter fullscreen mode Exit fullscreen mode

2. No Magic

Every function does exactly what it says. No hidden behavior, no gotchas.

import { pipe } from 'typetify/flow'

const result = pipe(
  5,
  n => n * 2,    // 10
  n => n + 1,    // 11
  n => `Result: ${n}` // 'Result: 11'
)
Enter fullscreen mode Exit fullscreen mode

Simple composition. No magic.

3. Tree-Shakable

Only bundle what you use.

// Import specific functions
import { pick } from 'typetify/object'
import { retry } from 'typetify/async'

// Or import from specific modules
import * as object from 'typetify/object'
Enter fullscreen mode Exit fullscreen mode

Your bundler will only include what you import.

4. Zero Dependencies

$ npm ls typetify
typetify@2.1.0
└── (no dependencies)
Enter fullscreen mode Exit fullscreen mode

No bloat. No supply chain risks. Just pure TypeScript.


Comparing to Alternatives

Feature Lodash Ramda Typetify
TypeScript-first
Runtime safety
Type narrowing
Zero dependencies
Tree-shakable ⚠️ Partial
Modern syntax ⚠️
Active maintenance ⚠️ Slowing

The Complete Feature Set

Typetify includes 18 modules covering:

  • Core - isDefined, assert, noop, identity
  • Guards - isString, isNumber, isObject, hasKey
  • Object - pick, omit, mapObject, get, set
  • Async - awaitTo, retry, debounce, throttle
  • Collection - unique, groupBy, partition, chunk
  • Input - safeJsonParse, parseNumber, parseBoolean
  • Flow - pipe, tap, match, tryCatch
  • String - String manipulation utilities
  • Math - Math utilities
  • Result - Result type pattern
  • Iterator - Iterator utilities
  • Decorator - TypeScript decorators
  • Logic - Logic utilities
  • Narrowing - Advanced type narrowing
  • Schema - Schema validation
  • DX - debug, invariant, measure, todo
  • Typed - Type utilities and branded types
  • Fn - Function utilities

See full documentation


Getting Started

npm install typetify
Enter fullscreen mode Exit fullscreen mode
import { isDefined, pick, awaitTo } from 'typetify'

// Or use the Lodash-style _ namespace
import { _ } from 'typetify'
const safe = _.pick(user, ['id', 'name'])
Enter fullscreen mode Exit fullscreen mode

What's Next?

I'd love to hear your feedback! Some areas I'm exploring:

  • 🔍 More schema validation utilities (like Zod-lite)
  • 🎯 Performance benchmarks vs Lodash/Ramda
  • 📦 Plugin system for custom utilities
  • 🧪 More real-world examples and recipes

Try It Out

🔗 GitHub: github.com/CodeSenior/typetify

📦 npm: npmjs.com/package/typetify

📚 Docs: typetify.hosby.io

Give it a try and let me know what you think! Stars ⭐ on GitHub are always appreciated.


Join the Discussion

What utility functions do you wish existed in TypeScript? Drop a comment below! 👇


P.S. We hit 477 downloads in the first 48 hours! 🚀 Thank you to everyone who's trying it out.

Top comments (3)

Collapse
 
matheus_releaserun profile image
Matheus

The pick() example is the one that really sold me. I've been bitten by that exact Lodash typo issue in production before - silently returning undefined for a misspelled key is one of those bugs that's genuinely hard to track down.

Curious about the awaitTo pattern. It looks similar to Go-style error handling. Do you find it scales well when you have multiple sequential async operations? In my experience the [error, data] destructuring gets a bit noisy when you have 4-5 of them in a row, but for isolated calls it's much cleaner than try/catch.

Also the isDefined filter callback is such a small thing but it's one of those TS paper cuts that comes up constantly. Nice to have it as a proper type guard out of the box.

Collapse
 
hosby profile image
Siddick FOFANA

Thanks for the feedback! Really glad the pick() example resonated — that silent undefined bug is exactly what pushed me to build this.

Great question about awaitTo with sequential operations. You're absolutely right that it can get noisy with 4-5 calls in a row. I've been experimenting with a few patterns to handle this:

Pattern 1: Early returns (cleanest for sequential)

async function processUser(id: string) {
  const [userError, user] = await awaitTo(fetchUser(id))
  if (userError) return { error: 'User fetch failed' }

  const [profileError, profile] = await awaitTo(fetchProfile(user.id))
  if (profileError) return { error: 'Profile fetch failed' }

  const [ordersError, orders] = await awaitTo(fetchOrders(user.id))
  if (ordersError) return { error: 'Orders fetch failed' }

  return { user, profile, orders }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Using pipe() for complex flows

import { pipe, awaitTo } from 'typetify'

const result = await pipe(
  userId,
  id => awaitTo(fetchUser(id)),
  ([err, user]) => err ? fail(err) : awaitTo(fetchProfile(user.id)),
  // ... chain continues
)
Enter fullscreen mode Exit fullscreen mode

Pattern 3: parallel() when order doesn't matter

import { parallel } from 'typetify/async'

const [user, profile, orders] = await parallel([
  () => fetchUser(id),
  () => fetchProfile(id),
  () => fetchOrders(id),
])
Enter fullscreen mode Exit fullscreen mode

I'm actually considering adding a sequence() helper for this exact use case:

const result = await sequence([
  () => fetchUser(id),
  (user) => fetchProfile(user.id),
  (profile) => fetchOrders(profile.userId)
])

if (!result.ok) {
  console.error(result.error) // First error encountered
  return
}

const [user, profile, orders] = result.data
Enter fullscreen mode Exit fullscreen mode

Would that be more ergonomic for your use cases? Open to ideas!

And yeah, isDefined is one of those "why isn't this in the standard library" moments 😄


P.S. If you have other sequential async patterns you use, I'd love to hear them. Always looking to improve the API based on real-world usage!

Collapse
 
hosby profile image
Siddick FOFANA • Edited

You know what, that's such good feedback that I just implemented it! 🚀

Check out the new sequence() utility in v4.3.0:

import { sequence } from 'typetify/async'

const result = await sequence([
  () => fetchUser(id),
  (user) => fetchProfile(user.id),
  (profile) => fetchOrders(profile.userId)
])

if (!result.ok) {
  console.error('Failed at step:', result.step, result.error)
  return
}

const [user, profile, orders] = result.data
Enter fullscreen mode Exit fullscreen mode

Let me know if this solves your use case! Always iterating based on community feedback.