Type IR
@rhi-zone/fractal-type-ir (packages/type-ir/) is a language-agnostic type representation plus a library of pure functions ("projectors") that render it into 60+ target languages, serialization libraries, schema formats, and wire formats. It's the shared IR fractal's codegen tooling (fractal-api-tree, compile.ts, doc generators) builds on.
The model
type TypeRef = {
readonly shape: TypeShape
readonly meta: Readonly<Record<string, unknown>>
}shapeis a discriminated union onkind— the structural payload ({ kind: "object", fields },{ kind: "array", element }, …).metais an open bag of side-channel conventions, not a fixed schema:optional,nullable,readonly,description,deprecated,typeName/declarationFile,brand, and anything a projector or extractor chooses to read. Nothing inindex.tsenumerates the full set — consumers agree on keys by convention, the same way the rest of the codebase treats metadata bags over fixed contracts.
TypeKinds — the kind vocabulary
The core structural + universal-primitive kinds live in TypeKinds (src/index.ts): boolean, number, integer, string, null, void, unknown, never, object, instance, array, stream, page, tuple, map, union, literal, enum, ref, intersection, function, method, interface.
A few are deliberately not subtypes of the kind they resemble, because collapsing them would lose information a capable projector needs:
instance(a class, nominal identity only —className/source) is not a subtype ofobject: a class's fields are only half its surface.stream(an async sequence) andpage(one window over a paginated collection) are not subtypes ofarray: they encode laziness/backpressure and pagination that a plain array can't express.interface(a method surface, like a Protobufservice) is not a subtype ofobject: its members are callables, not data fields.functionhas no parent.methodis the one kind that does register a parent (function) — see below.
Semantic refinements (int32/int64, float32/float64, uuid/uri, datetime/date/time, duration, bytes, …) are not in this core interface — they're independently importable extension modules under src/kinds/* (./kinds/int-widths, ./kinds/date-time, ./kinds/semantic-strings, ./kinds/bytes, etc., or the composite ./kinds/common bundling all of them) that augment TypeKinds via declaration merging and register their own parent relationship.
Subtyping hierarchy: ancestors/resolve/registerParent
export function registerParent(kind: string, parent: string | null): void
export function ancestors(kind: string): string[]
export function resolve<T>(kind: string, handlers: Record<string, T>): T | undefinedEach kind has at most one parent, tracked in a flat Record<string, string | null>. ancestors("int32") walks that chain (int32 → integer → number, say). resolve(kind, handlers) is what every projector calls to dispatch: look up kind in the handler table, and if it's missing, walk ancestors(kind) and use the first handler found. This is the fallback mechanism that lets a projector with no int32 handler still render an int32 correctly, by falling back to its integer (then number) handler — a kind extension module doesn't require every one of the 60+ projectors to be updated to stay correct, only the ones that care about the distinction.
registerParent is exported so extension modules (src/kinds/*, or a consumer's own declaration-merged kind) can wire themselves into the chain without editing index.ts.
Building a TypeRef
import { t, types } from "@rhi-zone/fractal-type-ir"
const user = t(types.object({
id: t(types.integer),
name: t(types.string),
email: t(types.string, { format: "email" }),
bio: { shape: types.string, meta: { optional: true } },
}))t(shape, meta?) wraps a shape into a TypeRef. types.* are shape constructors, one per kind (types.object(fields), types.array(element), types.union(variants), types.literal(value), …) — see src/index.ts for the full constructor list. Structural derive helpers (partial, required, pick, omit, extend, nullable, withMeta, deepPartial, deepRequired) transform an existing TypeRef into another one; see derive.md.
TypeRefDocument — named/recursive types
type TypeRefDocument = {
readonly root: TypeRef
readonly defs: Readonly<Record<string, TypeRef>>
}A bare TypeRef has no registry to resolve { kind: "ref", target } against. TypeRefDocument closes that gap: root plus every named definition refs in the tree point into. Every function that historically took a bare TypeRef still works — a TypeRef with no defs is just a document with no shared definitions. Doc-page projectors (doc-projectors.md) and several importers that produce multiple related types (e.g. from-protobuf) operate on TypeRefDocument rather than a bare TypeRef.
How projectors work
A projector is a pure function TypeRef => string (code-generating projectors) or TypeRef => <document> (schema/wire-format projectors that produce a structured object rather than source text). Internally, essentially every one follows the same shape:
const handlers: Record<string, Converter> = {
boolean: () => "boolean",
object: (shape) => { /* render fields */ },
array: (shape) => { /* render element */ },
// ...
}
export function toX(ref: TypeRef): string {
const converter = resolve(ref.shape.kind, handlers)
return converter === undefined ? /* fallback */ : converter(ref.shape)
}resolve (see above) means a projector only needs handlers for the kinds it actually cares to special-case; anything else falls back through the subtyping chain, and a kind with no ancestor and no handler degrades to whatever the projector considers its default "unknown" case (unknown in TypeScript, Any in Python, an opaque placeholder in a schema format, or in some cases an explicit throw for formats — like Cap'n Proto — that have no honest way to degrade a purely-nominal instance).
Reference pages
Each page below groups the projectors/importers for one language, format family, or transform family, with a worked example per variant:
- TypeScript — native, Zod, Valibot, TypeBox, io-ts, Yup, Superstruct, Runtypes, ArkType, Effect Schema, JSDoc
- Python — dataclasses, Pydantic, attrs, msgspec, cattrs
- Go — encoding/json, easyjson, jsoniter, sonic
- Java — Jackson, Gson, Moshi, JSON-B
- Kotlin — kotlinx.serialization, Jackson, Gson
- Swift — Codable, SwiftyJSON, ObjectMapper
- C# — System.Text.Json, Newtonsoft, ServiceStack
- C++ — nlohmann/json, RapidJSON, simdjson, Boost.JSON, Glaze
- Rust — serde
- Ruby — Sorbet, dry-types, RBS
- PHP — native, Symfony, JMS
- Dart — json_serializable, Freezed, built_value
- Other languages — Haskell (aeson), Elm, Flow, Objective-C (Foundation), Crystal
- Schema formats — JSON Schema (2020-12/07/04), OpenAPI (3.0/2.0), JSON Type Definition, Standard Schema
- Wire formats — Protobuf, Cap'n Proto, FlatBuffers, SQL DDL (+ MSSQL), GraphQL SDL, JSON-RPC
- Importers — the
from-*subpaths: format →TypeRef - Doc projectors — Docusaurus, Starlight, MkDocs reference-page generators
- Derive — structural
TypeRef → TypeReftransforms (partial,pick,nullable, …)