rtt
v1.3.4
Published
Runtime Typescript Types
Readme
rtt — Runtime Types for TypeScript
TypeScript types vanish at runtime.
rttbrings them back — with zero dependencies and a tiny footprint.
The Problem
When TypeScript compiles to JavaScript, all type information is stripped away. This creates a frustrating gap:
// ✅ At compile time — TypeScript knows this is a Dog
const dog: Dog = { name: 'Rover', breed: 'Labrador' };
// ❌ At runtime — it's just a plain object. No type info remains.
JSON.parse('{"name":"Rover","breed":"Labrador"}') // what IS this?Without runtime type information, you're forced into fragile workarounds: manual type strings, instanceof checks that don't survive serialization, or heavy validation libraries like Zod for every use case.
The Solution
rtt lets you define first-class type descriptors that carry both runtime identity and compile-time TypeScript types — enabling inheritance-aware runtime type checking with a single function call:
import {$type, is} from 'rtt';
// 1️⃣ Define your type hierarchy
const $Animal = $type<Animal>('app.Animal');
const $Dog = $type<Dog>('app.Dog', $Animal);
const $Cat = $type<Cat>('app.Cat', $Animal);
export type Animal = {$type: $Type; name: string};
export type Dog = Animal & { breed: string };
export type Cat = Animal & { lives: number };
// 2️⃣ Create typed objects with descriptor metadata
const rover: Dog = {$type: $Dog, name: 'Rover', breed: 'Labrador'};
const whiskers: Cat = {$type: $Cat, name: 'Whiskers', lives: 9};
// 3️⃣ Check types at runtime
is(rover, $Dog); // ✅ true
is(rover, $Animal); // ✅ true — inheritance works!
is(whiskers, $Dog); // ❌ falseFeatures
| Feature | Description |
|---|---|
| 🏗️ Type Hierarchies | Define parent-child relationships and check them at runtime |
| 🔍 Runtime Introspection | Know exactly what type an object is, even after JSON round-trips |
| ⚡ Zero Dependencies | No bloat — just two functions and a lightweight interface |
| 📦 Tiny Footprint | Minimal bundle size, tree-shakeable ESM exports |
| 🔒 Type Narrowing | is() returns a TypeScript type guard for safe downstream access |
| 🧬 Generic Type Support | Compare parameterized types with their arguments |
| 🏷️ Branded Types | Optional structural branding for compile-time + runtime safety |
Quick Start
Installation
npm install rtt
# or yarn add rtt / pnpm add rttType Narrowing
The is() function is a type guard — TypeScript narrows the type automatically:
const unknownUser: unknown = {$type: $User, name: 'John'};
if (is(unknownUser, $User)) {
// ✅ TypeScript knows user has 'name'
console.log(`Hello, ${unknownUser.name}`);
}Generic Types
Type descriptors work with generic types too — pass the inner type as a generics argument:
import {$type, is} from 'rtt';
// Define base types
const $User = $type<User>('app.User');
const $List = <T>($generic: $Type<T>) => $type<List<T>>('app.List', undefined, [$generic]);
export type User = {$type: $Type; name: string};
export type List<T> = {$type: $Type; items: T[]};
// Tag objects
const user: User = {$type: $User, name: 'John'};
const userList: List<User> = {$type: $List($User), items: [user]};
// Runtime checks preserve generic types
const unknownUser: unknown = user;
const unknownList: unknown = userList;
if (is(unknownUser, $User)) {
console.log(unknownUser.name); // ✅ ok
}
if (is(unknownList, $UserList)) {
console.log(unknownList.items[0].name); // ✅ ok — generic preserved!
}API Reference
$type<T>(name, parent?, generics?) → $Type<T>
Creates a named type descriptor that can be composed into hierarchies. The generic parameter T connects the runtime descriptor to your TypeScript type.
const $Animal = $type<Animal>('app.Animal');
const $Dog = $type<Dog>('app.Dog', $Animal);| Param | Type | Description |
|---|---|---|
| name | string | Unique type identifier (e.g. 'app.Dog') |
| parent? | $Type \| null | Parent type for inheritance chains |
| generics? | $Type[] | Generic type arguments to compare |
To tag a runtime object with a descriptor, spread it explicitly:
const rover: Dog = {$type: $Dog, name: 'Rover', breed: 'Labrador'};$nominal<I, P>(id, parent?, generics?)<T>() → $Nominal<I, T & P>
Creates a branded nominal type factory that produces values carrying $type metadata automatically.
const $UserId = $nominal('app.UserId')<{id: number}>();
const user = $UserId({id: 42});| Param | Type | Description |
|---|---|---|
| id | string | Unique nominal identifier |
| parent? | $Nominal<string, P> \| null | Parent nominal type for inheritance chains |
| generics? | $Type[] | Generic type arguments to compare |
is<T>(value, type) → value is T
Runtime type guard. Returns true if value.$type matches type or any of its ancestors in the hierarchy.
if (is(rover, $Dog)) {
// TypeScript narrows: rover is now typed as Dog
console.log(rover.name); // ✅ safe
}isType(a, b) → boolean
Low-level comparison between two $Type descriptors. Handles inheritance chains and generic type matching internally.
$Type<T> Interface
interface $Type<T = unknown> {
name: string;
type?: T;
parent?: $Type | null;
generics?: $Type[] | null;
}Brand<T> Type
A structural branding utility for compile-time + runtime safety:
import {Brand} from 'rtt';
type UserId = Brand<'UserId'> & number;Development
npm install
npm test # Run vitest suite
npm run build # Build with Vite + dts (generates dist/)