Skip to content

Type Safety ​

Wooks provides compile-time type safety with zero runtime overhead. Every primitive — key, cached, slot, defineEventKind, defineWook — carries its type through the entire system. There are no casts, no any escape hatches, and no runtime type checking.

How It Works ​

The core mechanism is type branding. Each slot stores a phantom type parameter _T that TypeScript tracks but JavaScript never sees:

ts
interface Key<T> {
  readonly _id: number
  readonly _name: string
  readonly _T?: T  // ← phantom type brand, not used at runtime
}

When you create a key, TypeScript locks in the type:

ts
const userId = key<string>('userId')
// userId is Key<string> — TypeScript remembers this everywhere

Typed Get/Set ​

EventContext.get() and EventContext.set() extract the type from the accessor:

ts
get<T>(accessor: Key<T> | Cached<T>): T
set<T>(key: Key<T> | Cached<T>, value: T): void

Both operations share the same <T>, tied to the accessor's type brand. TypeScript enforces consistency automatically:

ts
const userId = key<string>('userId')

ctx.set(userId, 'abc')    // ✓ string matches Key<string>
ctx.set(userId, 123)      // ✗ Type 'number' is not assignable to 'string'

const id = ctx.get(userId) // id is string — no cast needed

Inferred Factory Types ​

cached<T> and defineWook<T> infer their type from the factory function's return value — you never need to spell out the generic:

ts
// TypeScript infers T = number from the return value
const requestSize = cached((ctx) => {
  const req = ctx.get(httpKind.keys.req)
  return parseInt(req.headers['content-length'] || '0')
})

ctx.get(requestSize) // number — inferred, not annotated

The same applies to defineWook:

ts
// TypeScript infers the full return type from the factory
export const useJob = defineWook((ctx) => ({
  getJobId: () => ctx.get(jobKind.keys.jobId),
  getStatus: () => ctx.has(statusKey) ? ctx.get(statusKey) : 'pending',
}))

const { getJobId, getStatus } = useJob()
// getJobId: () => string
// getStatus: () => 'pending' | 'running' | 'done'

No manual type annotation. The factory return type flows through defineWook and becomes the wook's return type:

ts
function defineWook<T>(factory: (ctx: EventContext) => T): WookComposable<T>

WookComposable<T> is callable as (ctx?: EventContext) => T and exposes a readonly _slot — the underlying Cached slot, for advanced slot-isolation scenarios (see Event Context).

Parameterized Caching ​

cachedBy<K, V> tracks two independent types — the key and the value:

ts
const headerValue = cachedBy((name: string, ctx) => {
  return ctx.get(httpKind.keys.req).headers[name] || ''
})

headerValue('content-type')  // returns string
headerValue(42)              // ✗ Type 'number' is not assignable to 'string'

Event Kind Schemas ​

defineEventKind uses slot<T>() markers to declare a typed schema. Each slot is a zero-cost type brand:

ts
function slot<T>(): SlotMarker<T>  // returns {} at runtime — purely a type marker

When you pass a schema to defineEventKind, TypeScript uses conditional types with infer to transform each SlotMarker<T> into a Key<T>:

ts
type EventKind<S> = {
  keys: { [K in keyof S]: S[K] extends SlotMarker<infer V> ? Key<V> : never }
}

In practice:

ts
const jobKind = defineEventKind('job', {
  jobId: slot<string>(),
  payload: slot<unknown>(),
  priority: slot<number>(),
})

// TypeScript infers:
// jobKind.keys.jobId    → Key<string>
// jobKind.keys.payload  → Key<unknown>
// jobKind.keys.priority → Key<number>

ctx.get(jobKind.keys.priority) // number

Seed Validation ​

ctx.seed() enforces that every slot in the schema is provided with the correct type. The seed type is derived from the schema using a mapped conditional type:

ts
type EventKindSeeds<K> =
  K extends EventKind<infer S>
    ? { [P in keyof S]: S[P] extends SlotMarker<infer V> ? V : never }
    : never

This means:

ts
// ✓ All slots provided with correct types
ctx.seed(jobKind, {
  jobId: 'abc',
  payload: { data: 1 },
  priority: 5,
})

// ✗ Missing 'priority'
ctx.seed(jobKind, {
  jobId: 'abc',
  payload: { data: 1 },
})

// ✗ Wrong type for 'priority'
ctx.seed(jobKind, {
  jobId: 'abc',
  payload: { data: 1 },
  priority: 'high',  // Type 'string' is not assignable to 'number'
})

No runtime validation code. The compiler catches mismatches before you run anything.

Summary ​

PrimitiveType mechanismWhat it enforces
key<T>()Phantom type brandget/set must use the same type
cached<T>(fn)Inferred from factory returnRead type matches computed type
cachedBy<K,V>(fn)Two independent genericsKey and value types from factory signature
slot<T>()Type-level marker (zero runtime cost)Schema slot type for defineEventKind
defineEventKindMapped type with inferTransforms SlotMarker<T> → Key<T> for each slot
defineWook<T>(fn)Inferred from factory returnWook return type matches factory return type
ctx.seed()Conditional mapped typeAll required slots present with correct types

All type safety is compile-time only — no runtime checks, no instanceof, no validation libraries. The context is a flat Map<number, unknown> at runtime, with TypeScript enforcing correctness through generics, phantom types, and conditional type inference.

Released under the MIT License.