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

unacy

v0.7.0

Published

Type-safe unit and format conversion library

Readme

@unacy/core

Type-safe unit and format conversion library with automatic multi-hop composition.

Features

  • 🔒 Type-safe conversions - Compile-time checks prevent mixing incompatible units
  • 🔄 Auto-composition - Automatically chains converters (A→B→C for A→C)
  • 🚀 Zero runtime overhead - Phantom types have no performance cost
  • 🎯 Shortest path - BFS finds optimal conversion routes
  • 🛡️ Cycle detection - Prevents infinite conversion loops
  • 📦 Tree-shakeable - Only bundle converters you use
  • Fluent API - Clean, readable conversion syntax
  • 🎯 Typed Metadata - Native support for number, string, boolean, and bigint units

Installation

pnpm add @unacy/core

Quick Start

import { createRegistry } from '@unacy/core';
import type { WithTypedUnits } from '@unacy/core';

// Define metadata for your units (name + type)
const CelsiusMetadata = {
  name: 'Celsius' as const,
  type: 'number' as const
};

const FahrenheitMetadata = {
  name: 'Fahrenheit' as const,
  type: 'number' as const
};

// Define your unit types with metadata
type Celsius = WithTypedUnits<typeof CelsiusMetadata>;
type Fahrenheit = WithTypedUnits<typeof FahrenheitMetadata>;

// Create a registry and register converters
const tempRegistry = createRegistry()
  .register(CelsiusMetadata, FahrenheitMetadata, (c) => ((c * 9/5) + 32));

// Create branded values using callable accessors (NEW!)
const temp = tempRegistry.Celsius(25); // Returns Celsius type

// Convert with type safety - two ways:

// Method 1: unit accessor API
const fahrenheit1 = tempRegistry.Celsius.to.Fahrenheit(temp);

// Method 2: fluent callable accessor API
const fahrenheit2 = tempRegistry.Celsius.to.Fahrenheit(tempRegistry.Celsius(30));

console.log(fahrenheit1); // 77
console.log(fahrenheit2); // 86

// Old way still works (manual casting)
const tempOld: Celsius = 25 as Celsius;

Usage Examples

Callable Unit Accessors

Unit accessors are now callable functions that create branded values:

// Create branded values without manual type casting
const temp = registry.Celsius(25);        // Returns WithTypedUnits<typeof CelsiusMetadata>
const distance = registry.meters(100);    // Returns WithTypedUnits<typeof MetersMetadata>

// Fluent workflow
const fahrenheit = registry.Celsius.to.Fahrenheit(registry.Celsius(20));

// Compare with old way (still works)
const tempOld: Celsius = 25 as Celsius;

// Benefits:
// - Cleaner syntax
// - Less verbose than manual casting
// - Type-safe by design
// - Works seamlessly with conversions

Typed Metadata

Define metadata with minimal required fields (name + type):

const CelsiusMetadata = {
  name: 'Celsius' as const,
  type: 'number' as const
};

const EtherMetadata = {
  name: 'ether' as const,
  type: 'bigint' as const
};

const FlagMetadata = {
  name: 'enabled' as const,
  type: 'boolean' as const
};

Basic Unit Conversions

// Same registry as above
const distance: Meters = 10 as Meters;

// Access units directly via property syntax
const feet = distanceRegistry.meters.to.feet(distance);
console.log(feet); // 32.8084

// Or use callable accessors
const feet2 = distanceRegistry.meters.to.feet(distanceRegistry.meters(10));

// Works in both directions
const meters = distanceRegistry.feet.to.meters(32.8084 as Feet) satisfies Meters;
console.log(meters); // 10

Bidirectional Converters

import { createRegistry } from '@unacy/core';
import type { WithTypedUnits } from '@unacy/core';

const MetersMetadata = {
  name: 'meters' as const,
  type: 'number' as const
};

const KilometersMetadata = {
  name: 'kilometers' as const,
  type: 'number' as const
};

type Meters = WithTypedUnits<typeof MetersMetadata>;
type Kilometers = WithTypedUnits<typeof KilometersMetadata>;

const registry = createRegistry()
  .register(MetersMetadata, KilometersMetadata, {
    to: (m: number) => (m / 1000),
    from: (km: number) => (km * 1000)
  });

// Both directions work automatically
const km = registry.convert(5000 as Meters, 'meters').to('kilometers'); // 5
const m = registry.convert(5 as Kilometers, 'kilometers').to('meters'); // 5000

Multi-Hop Auto-Composition

The registry automatically composes converters via shortest path:

const MetersMetadata = { name: 'meters' as const, type: 'number' as const };
const KilometersMetadata = { name: 'kilometers' as const, type: 'number' as const };
const MilesMetadata = { name: 'miles' as const, type: 'number' as const };

type Meters = WithTypedUnits<typeof MetersMetadata>;
type Kilometers = WithTypedUnits<typeof KilometersMetadata>;
type Miles = WithTypedUnits<typeof MilesMetadata>;

const registry = createRegistry()
  .register(MetersMetadata, KilometersMetadata, {
    to: (m: number) => (m / 1000),
    from: (km: number) => (km * 1000)
  })
  .register(KilometersMetadata, MilesMetadata, {
    to: (km: number) => (km * 0.621371),
    from: (mi: number) => (mi / 0.621371)
  });

// No direct meters→miles converter registered!
// Registry auto-composes: meters → kilometers → miles
const meters: Meters = 5000 as Meters;
const miles = registry.convert(meters, 'meters').to('miles');
console.log(miles); // 3.106855

Format-Tagged Values

import { type WithFormat, type FormatterParser, ParseError } from '@unacy/core';
import { z } from 'zod';

type ISO8601 = WithFormat<Date, 'ISO8601'>;

const iso8601: FormatterParser<ISO8601> = {
  format: (date) => date.toISOString(),
  parse: (input) => {
    const schema = z.string().datetime();
    try {
      return new Date(schema.parse(input)) as ISO8601;
    } catch {
      throw new ParseError('ISO8601', input, 'Invalid date format');
    }
  }
};

// Format
const now: ISO8601 = new Date() as ISO8601;
const str = iso8601.format(now); // "2026-01-06T12:00:00.000Z"

// Parse
const date = iso8601.parse('2026-01-06T12:00:00.000Z');

API Reference

Types

WithTypedUnits<M extends TypedMetadata<T>>

Brand a value with strongly-typed metadata for compile-time unit safety.

const CelsiusMetadata = { name: 'Celsius' as const, type: 'number' as const };
type Celsius = WithTypedUnits<typeof CelsiusMetadata>;
const temp: Celsius = tempRegistry.Celsius(25);

WithUnits<T, U>

Legacy: Brand a value with a unit identifier for compile-time safety.

type Celsius = WithUnits<number, 'Celsius'>;
const temp: Celsius = 25 as Celsius;

TypedMetadata<T>

Minimal metadata type with name and type information.

type NumericMetadata = TypedMetadata<number>;
// { name: string; type: 'number' }

type StringMetadata = TypedMetadata<string>;
// { name: string; type: 'string' }

WithFormat<T, F>

Brand a value with a format identifier for serialization safety.

type ISO8601 = WithFormat<Date, 'ISO8601'>;
const date: ISO8601 = new Date() as ISO8601;

Converter<TInput, TOutput>

Unidirectional converter function.

const c2f: Converter<Celsius, Fahrenheit> = (c) =>
  ((c * 9/5) + 32) as Fahrenheit;

BidirectionalConverter<TInput, TOutput>

Pair of converters for two-way transformations.

const meterKm: BidirectionalConverter<Meters, Kilometers> = {
  to: (m: number) => (m / 1000),
  from: (km: number) => (km * 1000)
};

Registry

createRegistry<Units>()

Create a new converter registry.

const registry = createRegistry<'A' | 'B' | 'C'>();

register(from, to, converter)

Register a unidirectional converter.

registry.register(CelsiusMetadata, FahrenheitMetadata, celsiusToFahrenheit);

register(from, to, converter) (bidirectional)

Register both directions at once.

registry.register(MetersMetadata, KilometersMetadata, meterKm);

convert(value, fromUnit).to(toUnit)

Fluent API for type-safe conversions.

const result = registry.convert(value, 'Celsius').to('Fahrenheit');

Errors

  • UnacyError - Base error class
  • CycleError - Cycle detected in conversion graph
  • MaxDepthError - Exceeded maximum conversion depth (5 hops)
  • ConversionError - No conversion path found
  • ParseError - Invalid input during parsing

Best Practices

  1. Define metadata as const at module boundaries for consistency
  2. Use bidirectional converters when both directions are needed
  3. Document precision loss in converters
  4. Cache registries for performance
  5. Use WithTypedUnits for brand-new code; leverage type inference
  6. Validate with Zod in parsers

Performance

  • Direct conversions: O(1) lookup
  • Multi-hop conversions: O(V+E) BFS with caching
  • Type checking: <1s for typical graphs (<100 units)
  • Zero runtime overhead for type brands

License

MIT

Links