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
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
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'
}
}
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
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
}
}
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 }
}
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
}
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
}
}
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[] ✅
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
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!
}
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'
)
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'
Your bundler will only include what you import.
4. Zero Dependencies
$ npm ls typetify
typetify@2.1.0
└── (no dependencies)
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
Getting Started
npm install typetify
import { isDefined, pick, awaitTo } from 'typetify'
// Or use the Lodash-style _ namespace
import { _ } from 'typetify'
const safe = _.pick(user, ['id', 'name'])
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)
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.
Thanks for the feedback! Really glad the
pick()example resonated — that silentundefinedbug is exactly what pushed me to build this.Great question about
awaitTowith 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)
Pattern 2: Using
pipe()for complex flowsPattern 3:
parallel()when order doesn't matterI'm actually considering adding a
sequence()helper for this exact use case:Would that be more ergonomic for your use cases? Open to ideas!
And yeah,
isDefinedis 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!
You know what, that's such good feedback that I just implemented it! 🚀
Check out the new
sequence()utility in v4.3.0:Let me know if this solves your use case! Always iterating based on community feedback.