farm-plugin-reflect
v0.2.4
Published
C#-style reflection for FarmFE — type_of<T>(), isOfType<T>(), union/intersection/type alias support, cross-package FQN registry
Maintainers
Readme
farm-plugin-reflect
C#-style reflection for FarmFE — type_of<T>() returns TypeInfo at runtime, isOfType<T>(obj) / isInterface<T>(obj) generate runtime type guards, classes implementing IObject auto-generate get_typeinfo(). Supports union types, intersection types, type aliases, and cross-package metadata sharing via a global runtime registry with FQN namespacing.
Why?
TypeScript types are erased at runtime. You can't inspect an interface's properties, check if an object conforms to a type, or enumerate enum members at runtime — until now. This plugin provides:
- Reflection —
type_of<T>()returns a richTypeInfoobject (like C#'sSystem.Type) - Type guards —
isOfType<T>(obj)checks if an object matches a type at runtime (interfaces, classes, enums, unions, intersections) - IObject — classes implementing
IObjectauto-getget_typeinfo()method - Cross-package sharing — reflection metadata is registered in a global singleton registry, keyed by FQN (
pkg::Type), so types defined in one package can be used in another without re-generation
Install
pnpm add -D farm-plugin-reflectUsage
1. Define types
// types.ts
export interface IUser {
name: string
age: number
email?: string
roles: string[]
}
export interface IAdmin extends IUser {
permissions: string[]
}
export enum Role {
Viewer = 0,
Editor = 1,
Admin = 2,
}
// Union types
export type Status = 'active' | 'inactive' | 'suspended'
// Intersection types
export type AdminUser = IUser & { permissions: string[] }
// Type aliases
export type UserId = string2. Use type_of<T>() for runtime type info
// main.ts
import type { IUser, IAdmin, Role, Status, AdminUser } from './types'
const info = type_of<IUser>()
console.log(info.name) // "IUser"
console.log(info.kind) // "interface"
console.log(info.properties) // [{ name: "name", type: TypeInfo(string), ... }, ...]
console.log(info.interfaces) // [] (no extends)
console.log(info.isInterface) // true
const adminInfo = type_of<IAdmin>()
console.log(adminInfo.interfaces) // ["IUser"]
const roleInfo = type_of<Role>()
console.log(roleInfo.kind) // "enum"
// Union and intersection types work too
const statusInfo = type_of<Status>()
console.log(statusInfo.kind) // "union"
const adminUserInfo = type_of<AdminUser>()
console.log(adminUserInfo.kind) // "intersection"3. Use isOfType<T>(obj) for runtime type guards
Works for interfaces, classes, enums, unions, and intersections:
const data: unknown = JSON.parse(input)
// Check against interface
if (isOfType<IUser>(data)) {
console.log(data.name) // ✅ type-safe
}
// Same as isInterface<T>() — alias for interface types
if (isInterface<IUser>(data)) {
console.log(data.age) // ✅ type-safe
}
// Check against enum
if (isOfType<Role>(value)) {
// value is one of Role's values
}
// Check against class
if (isOfType<User>(obj)) {
// obj is an instance of User
}
// Union type guard
if (isOfType<Status>(val)) {
// val is 'active' | 'inactive' | 'suspended'
}
// Intersection type guard — all parts must match
if (isOfType<AdminUser>(obj)) {
// obj matches IUser AND has permissions
}4. Implement IObject for class instances
// user.ts
export class User implements IObject, IUser {
name: string
age: number
email?: string
roles: string[]
constructor(name: string, age: number) {
this.name = name
this.age = age
this.roles = []
}
// get_typeinfo() is AUTO-GENERATED by the plugin
}
const user = new User('Alice', 30)
const info = user.get_typeinfo()
console.log(info.name) // "User"
console.log(info.kind) // "class"
console.log(info.properties) // [{ name: "name", ... }, { name: "age", ... }, ...]
console.log(info.interfaces) // ["IObject", "IUser"]
console.log(info.baseType) // null5. Configure FarmFE
// farm.config.ts
import { defineConfig } from '@farmfe/core'
import reflect from 'farm-plugin-reflect'
export default defineConfig({
plugins: [reflect()],
})Cross-Package Reflection
The Problem
If package @myorg/types defines IUser, and package @myorg/app calls type_of<IUser>(), the reflection metadata must be available in @myorg/app without re-generating it.
The Solution: FQN + Global Registry + reflect-usage
Every type gets a Fully Qualified Name (FQN) based on its package: @myorg/types::IUser. All TypeInfo and guards are registered in a global singleton registry (__reflectRegistry), ensuring the same instance is shared across all modules and packages.
Provider package (defines types)
- Add
"reflect-usage": trueto yourpackage.json:
{
"name": "@myorg/types",
"reflect-usage": true
}- The plugin automatically generates a
reflect-registersubpath export that registers all types from this package into the global registry.
Consumer package (uses types from another package)
The plugin auto-discovers dependencies with "reflect-usage": true in their package.json and imports their register files. No configuration needed — just use the types:
// @myorg/app — automatically finds @myorg/types' registry
import type { IUser } from '@myorg/types'
const info = type_of<IUser>() // ✅ works — looks up "@myorg/types::IUser" in registryFQN Format
| Package | Type | FQN |
|---------|------|-----|
| @myorg/types | IUser | @myorg/types::IUser |
| @myorg/types | Role | @myorg/types::Role |
| my-lib | Config | my-lib::Config |
| (current project) | LocalType | <pkgName>::LocalType |
The pkgPrefix is auto-detected from your package.json name field, or can be set manually.
Registry API
The runtime registry (__reflectRegistry) is available globally:
// These are internal — you normally use type_of<T>() and isOfType<T>() instead
__reflectRegistry.register(fqn, typeInfo, guard?) // Register a type
__reflectRegistry.getTypeInfo(fqn) // Get TypeInfo by FQN
__reflectRegistry.isOfType(fqn, obj) // Check object against registered guard
__reflectRegistry.has(fqn) // Check if type is registered
__reflectRegistry.list() // List all registered FQNsAPI Summary
| API | Returns | Description |
|-----|---------|-------------|
| type_of<T>() | TypeInfo | Get runtime type metadata |
| isOfType<T>(obj) | boolean | Runtime type guard (interfaces, classes, enums, unions, intersections) |
| isInterface<T>(obj) | boolean | Alias for isOfType<T>() — for interface types |
| obj.get_typeinfo() | TypeInfo | Only on IObject class instances |
TypeInfo API
interface TypeInfo {
name: string
fqn: string // Fully qualified name: "pkg::Type"
kind: 'class' | 'interface' | 'enum' | 'primitive' | 'array' | 'union' | 'intersection' | 'object' | 'function' | 'literal' | 'typeAlias' | 'unknown'
fullName: string
properties: PropertyInfo[]
methods: MethodInfo[]
interfaces: string[]
baseType: string | null
genericArgs: TypeInfo[]
isInterface: boolean
isOptional: boolean
metadata: Record<string, unknown>
}
interface PropertyInfo {
name: string
type: TypeInfo
optional: boolean
readonly: boolean
hasGetter: boolean
hasSetter: boolean
metadata: Record<string, unknown>
}
interface MethodInfo {
name: string
returnType: TypeInfo
parameters: ParameterInfo[]
metadata: Record<string, unknown>
}
interface ParameterInfo {
name: string
type: TypeInfo
optional: boolean
hasDefault: boolean
}Supported Types
| TypeScript Type | TypeInfo.kind | isOfType check |
|---|---|---|
| interface IX { ... } | 'interface' | Property-by-property check |
| class X { ... } | 'class' | instanceof + constructor name + property check |
| enum X { ... } | 'enum' | Value in enum values list |
| type X = ... | 'typeAlias' | Resolves to target type |
| string, number, boolean | 'primitive' | typeof check |
| T[] / Array<T> | 'array' | Array.isArray + element check |
| [A, B, C] (tuple) | 'array' | Length + element checks |
| A \| B (union) | 'union' | Any member matches |
| A & B (intersection) | 'intersection' | All members match |
| 'literal' / 123 / true | 'literal' | Strict equality |
| Record<K, V> | 'object' | Values check |
| Partial<T>, Required<T>, Readonly<T> | 'object' | Mapped property modifiers |
| Pick<T, K>, Omit<T, K> | 'object' | Filtered properties |
| Function | 'function' | typeof === 'function' |
| any, unknown | 'unknown' | Always true |
| Generic IX<T> | 'interface' | Factory function |
IObject
IObject is a marker interface. When a class implements IObject, the plugin auto-generates a get_typeinfo() method.
class User implements IObject { ... } // ✅ has get_typeinfo()
class PlainUser { ... } // ❌ no get_typeinfo()isOfType vs isInterface
isInterface<T>(obj) is an alias for isOfType<T>(obj). Both resolve to the same registry-based guard. Use whichever reads better in context:
isInterface<IUser>(data)— when checking against an interfaceisOfType<User>(data)— when checking against a class, enum, union, or any type
Options
reflect({
// File patterns
include: [], // File patterns to include
exclude: ['node_modules'], // File patterns to exclude
// Package prefix for FQN (auto-detected from package.json if not set)
pkgPrefix: '@myorg/types', // → FQN: "@myorg/types::IUser"
// Auto-discover reflect-usage packages (default: true)
autoDiscover: true,
// Control which complex types are expanded
resolveOpts: {
expandTypes: true, // Master switch for complex type expansion
expandTypeAliases: true, // Expand type aliases
expandIntersection: true, // Expand intersection types
expandUnion: true, // Expand union types
},
})resolveOpts — Controlling Output Size
If you want minimal generated code, disable complex type expansion:
reflect({
resolveOpts: {
expandTypes: false, // Skip all complex type resolution
// Or individually:
expandUnion: false, // Union types → 'unknown'
expandIntersection: false, // Intersection types → 'unknown'
expandTypeAliases: false, // Type aliases → not generated
},
})On-Demand Generation
The plugin only generates TypeInfo and guards for types that are actually used — i.e., types referenced in type_of<T>(), isOfType<T>(), isInterface<T>(), or classes implementing IObject. Unused types produce zero output.
Relation to farm-plugin-isinterface
farm-plugin-isinterface is the standalone version with only isInterface<T>(). farm-plugin-reflect is the full version that includes isInterface<T>() + isOfType<T>() + type_of<T>() + IObject + cross-package registry. Don't use both together — farm-plugin-reflect is a superset.
License
MIT
