The Definition Is the Interface
Computed values usually begin as helper functions beside plain data. That is fine until the helper becomes part of the object contract: display names, package lists, summaries, labels, or derived fields shared across many definitions.
CObject asks whether those values can live inside the object definition without losing plain-object composition. A definition can carry literals, arrays, nested objects, spreads, and functions. Evaluation returns an object that reads naturally, while function fields become computed values.
The important split is definition type versus result type. Authors write a shape that may contain functions. Callers read a shape where those functions have become values.
const base = {
result: ({ a, b }) => a + b,
};
const computedA = CObject({
...base,
a: "a",
b: "b",
});
const computedB = CObject({
...base,
a: "hello",
b: " world",
});
computedA.result; // "ab"
computedB.result; // "hello world"Evaluation Is a Descriptor Pass
The runtime is small because JavaScript already has the primitive it needs. CObject creates a result object, walks the own property names, turns functions into getters, recurses into nested objects and arrays, and defines ordinary values through descriptors.
There is no proxy layer and no dependency graph. Computation is lazy because accessors run on read. Nested values are evaluated with the same root object, so a computed field can derive from branches outside its local subtree.
function evaluate<Obj, Root>(object: Obj, root?: Root): Root {
const result = createObject(object);
const keys = getKeysOf(object);
if (!root) root = result as Root;
for (const key of keys) {
const property = object[key];
if (isFunc(property)) {
Object.defineProperty(
result,
key,
makePropertyDescriptor({
get() {
return property(root);
},
}),
);
continue;
}
if (Array.isArray(property) || isObj(property)) {
Object.defineProperty(
result,
key,
makePropertyDescriptor({ value: evaluate(property, root) }),
);
}
}
return result;
}Types Turn Definitions Into Values
The type-level experiment is the real interface. CObject<T> maps function fields to their return types and recursively maps nested objects. CObjectDef<T, Parent> describes what authors are allowed to write before evaluation.
That means result.c can be typed as a number when c was defined as a function returning a number. The evaluated object is concrete even though the authoring object was mixed data and computation.
type CObject<T> = {
[K in keyof T]: T[K] extends (arg: T) => infer Return
? Return
: T[K] extends object
? CObject<T[K]>
: T[K];
};
type CObjectDef<T, Parent = T> = {
[K in keyof T]: T[K] extends (arg: any) => infer Return
? (arg: Exclude<T & Parent, Function>) => Return
: T[K] extends object
? CObjectDef<T[K], Parent>
: T[K];
};The Boundary Is the Hard Part
The caveat is important. The runtime can technically read another computed getter, but the type system excludes function fields from computed arguments. That is the right conservative choice because dependency order, cycles, memoization, invalidation, and computed-to-computed semantics are not solved by the current implementation.
CObject is valuable because it keeps the small idea honest: descriptor-backed lazy fields, root-aware nested evaluation, plain object composition, and a mapped result type. It is not a reactive engine, async graph, or state manager. It is a compact experiment in making derived object state explicit.