npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@herodot-app/idion

v0.1.3

Published

Idion is a typescript library that gives your domain objects a proper identity — not just a shape, but a name they can be proud of

Readme

idion

From Ancient Greek ἴδιον (idion) — "one's own", "particular", "that which belongs to oneself". Not to be confused with idiot, which shares the same root but took a very different career path.

Idion gives your domain objects a proper identity — not just a shape, but a name they can be proud of.


Why?

TypeScript's structural type system is powerful, but it has a blind spot: two objects with identical shapes are considered the same type, even if they represent completely different things.

type UserId = { value: string }
type PostId = { value: string }

declare const userId: UserId

const postId: PostId = userId // TypeScript is perfectly fine with this.
                              // Your domain model is not.

This is the classic "stringly typed" trap in disguise. You end up writing runtime checks, defensive guards, and the occasional panicked comment saying // DON'T PASS A POST ID HERE.

Idion solves this by branding objects at both the type level and the runtime level. Each branded object carries a hidden identity — a symbol stamp — that TypeScript tracks statically and that you can inspect at runtime:

type UserId = Idion<'UserId', { value: string }>
type PostId = Idion<'PostId', { value: string }>

declare const userId: UserId

const postId: PostId = userId // Type error. They know who they are.

No wrappers. No classes. No ceremony. Just a plain object that knows its own name.


When to use it

Use Idion when:

  • You have multiple types that share the same structure but mean different things (UserId vs PostId, EuroAmount vs DollarAmount, RawHtml vs SafeHtml).
  • You want domain boundaries to be enforced by the compiler, not by convention and hope.
  • You need to distinguish branded values at runtime — for example in a type guard, a validation layer, or a serializer.
  • You want branded types without the overhead of wrapper classes or the fragility of plain type aliases.

Installation

bun add @herodot-app/idion

Idion is a pure TypeScript utility with no runtime dependencies. It requires TypeScript 5+.


How to use it

Define your branded types

Declare your domain identity types using the Idion generic. The first parameter is the brand (a string literal), the second is the base object shape:

import { Idion } from '@herodot-app/idion'

type UserId = Idion<'UserId', { value: string }>
type PostId = Idion<'PostId', { value: string }>
type EuroAmount = Idion<'EuroAmount', { value: number; currency: 'EUR' }>

Create branded values

Use Idion.create to stamp a brand onto a plain object. The brand is added in-place via Object.assign — no cloning, no wrapping:

const userId = Idion.create({ id: 'UserId', value: { value: 'abc-123' } })
//    ^? Idion<'UserId', { value: string }>

const postId = Idion.create({ id: 'PostId', value: { value: 'xyz-456' } })
//    ^? Idion<'PostId', { value: string }>

Now TypeScript will refuse to mix them up:

function getUser(id: UserId) { /* ... */ }

getUser(postId) // Type error — PostId is not assignable to UserId.
getUser(userId) // All good.

Narrow at runtime with Idion.is

When you receive a value from an external source (an API, a message queue, user input), use Idion.is to confirm its identity before trusting it:

// Check for any brand — "is this one of ours?"
if (Idion.is(unknownValue)) {
  // unknownValue carries some Idion brand
}

// Check for a specific brand — "is this exactly a UserId?"
if (Idion.is(unknownValue, 'UserId')) {
  // TypeScript now knows unknownValue is Idion<'UserId', typeof unknownValue>
  console.log(unknownValue.value)
}

Use symbols as brands for truly private identities

String brands are readable and great for debugging. But if you need a brand that is completely unguessable — one that only code holding a direct reference to the symbol can produce — use a symbol instead:

const SessionTokenBrand = Symbol('SessionToken')
type SessionToken = Idion<typeof SessionTokenBrand, { raw: string }>

const token = Idion.create({ id: SessionTokenBrand, value: { raw: 's3cr3t' } })

// Nobody outside this module can forge a SessionToken without the symbol.

Inherit multiple brands

An object can hold more than one brand. This is useful when a value naturally belongs to several identities at once — think of a PremiumUser that is also, unambiguously, a User.

To achieve this, spread or Object.assign an existing Idion into the value of a new one. The resulting object carries every brand from its ancestors, plus the new one. It has layers. It contains multitudes.

const user = Idion.create({ id: 'User', value: { id: 'abc-123', name: 'Alice' } })
//    ^? Idion<'User', { id: string; name: string }>

const premiumUser = Idion.create({
  id: 'PremiumUser',
  value: Object.assign({ plan: 'gold' }, user),
})
//    ^? Idion<'PremiumUser', { plan: string } & Idion<'User', { id: string; name: string }>>

Idion.is(premiumUser, 'User')        // true — still very much a User
Idion.is(premiumUser, 'PremiumUser') // true — and proud of it
Idion.is(user, 'PremiumUser')        // false — not everyone gets the upgrade

The inheritance is structural: all brand keys from the source object are copied verbatim. There is no magic lineage tracking — just plain objects doing what plain objects do best.


API reference

Idion<I, T>

A type alias for T & { [Idion.identifier]: I }. Combines your base shape with a hidden brand property.

| Parameter | Constraint | Description | |-----------|------------|-------------| | I | string \| symbol | The brand that gives the object its identity. | | T | {} | The base object shape carrying the actual data. |

Idion.create({ id, value })

Stamps id onto value and returns it as a fully typed Idion<I, T>. The original reference is mutated in place.

Idion.is(value, id?)

Type guard that narrows value to Idion<I, T>. Without id, confirms any brand is present. With id, also confirms the brand matches exactly.

Idion.identifier

The well-known symbol (Symbol.for('@herodot-app/idion/identifier')) used as the property key for the brand. Consistent across module boundaries and bundler shenanigans.