Skip to content

Commit a5ad113

Browse files
authored
feat(api): Extend Opaque<T> to accept all BrandedCell types (#2007)
feat(api): Extend Opaque<T> to accept all BrandedCell types Expands the Opaque<T> type to accept any kind of BrandedCell wrapping T, not just OpaqueRef<T>. This allows more flexible type handling across the codebase by accepting Cell<T>, OpaqueCell<T>, Stream<T>, ComparableCell<T>, ReadonlyCell<T>, and WriteonlyCell<T> as valid Opaque values. Also refines RenderNode type to explicitly accept BrandedCell<InnerRenderNode> rather than the overly permissive Opaque<any>, improving type safety in view rendering. Updates tests to reflect the new type capabilities, particularly in schema-to-ts.test.ts where nested cell types are now properly handled. Progress on recipe construction rollout plan: - Completed: Opaque<T> accepts T or any CellLike<T> at any nesting level - Completed: Accept any T where any sub part of T can be wrapped in one or more BrandedCell (for inputs to node factories)
1 parent f4c7921 commit a5ad113

File tree

6 files changed

+35
-16
lines changed

6 files changed

+35
-16
lines changed

docs/specs/recipe-construction/rollout-plan.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
behavior, i.e. each key is an `OpaqueRef` again. That's just for now, until
1616
the AST does a .key transformation under the hood.
1717
- [x] Update `CellLike` to be based on `BrandedCell` but allow nesting.
18-
- [ ] `Opaque<T>` accepts `T` or any `CellLike<T>` at any nesting level
18+
- [x] `Opaque<T>` accepts `T` or any `CellLike<T>` at any nesting level
1919
- [ ] Simplify most wrap/unwrap types to use `CellLike`. We need
20-
- [ ] "Accept any T where any sub part of T can be wrapped in one or more
20+
- [x] "Accept any T where any sub part of T can be wrapped in one or more
2121
`BrandedCell`" (for inputs to node factories)
2222
- [ ] "Strip any `BrandedCell` from T and then wrap it in OpaqueRef<>" (for
2323
outputs of node factories, where T is the output of the inner function)

packages/api/index.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,17 @@ type MaybeCellWrapped<T> =
388388
*/
389389
export type Opaque<T> =
390390
| T
391+
// We have to list them explicitly so Typescript can unwrap them. Doesn't seem
392+
// to work if we just say BrandedCell<T>
391393
| OpaqueRef<T>
394+
| AnyCell<T>
395+
| BrandedCell<T>
396+
| OpaqueCell<T>
397+
| Cell<T>
398+
| Stream<T>
399+
| ComparableCell<T>
400+
| ReadonlyCell<T>
401+
| WriteonlyCell<T>
392402
| (T extends Array<infer U> ? Array<Opaque<U>>
393403
: T extends object ? { [K in keyof T]: Opaque<T[K]> }
394404
: T);
@@ -1273,14 +1283,16 @@ export type Props = {
12731283

12741284
/** A child in a view can be one of a few things */
12751285
export type RenderNode =
1286+
| InnerRenderNode
1287+
| BrandedCell<InnerRenderNode>
1288+
| Array<RenderNode>;
1289+
1290+
type InnerRenderNode =
12761291
| VNode
12771292
| string
12781293
| number
12791294
| boolean
1280-
| Cell<RenderNode>
1281-
| undefined
1282-
| Opaque<any>
1283-
| RenderNode[];
1295+
| undefined;
12841296

12851297
/** A "virtual view node", e.g. a virtual DOM element */
12861298
export type VNode = {

packages/html/test/html-recipes.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type Cell,
66
createBuilder,
77
type IExtendedStorageTransaction,
8-
Opaque,
8+
type OpaqueRef,
99
Runtime,
1010
} from "@commontools/runner";
1111
import { StorageManager } from "@commontools/runner/storage/cache.deno";
@@ -98,7 +98,7 @@ describe("recipes with HTML", () => {
9898
h(
9999
"ul",
100100
null,
101-
items.map((item: Opaque<Item>, i: Opaque<number>) =>
101+
items.map((item, i: number) =>
102102
h("li", { key: i.toString() }, item.title)
103103
) as VNode[],
104104
),
@@ -213,12 +213,12 @@ describe("recipes with HTML", () => {
213213
[UI]: h(
214214
"div",
215215
null,
216-
data.map((row: Opaque<Record<string, unknown>>) =>
216+
data.map((row: Record<string, unknown>) =>
217217
h(
218218
"ul",
219219
null,
220-
entries(row).map((input: Opaque<[string, unknown]>) =>
221-
h("li", null, [input[0] as string, ": ", str`${input[1]}`])
220+
entries(row).map((input: OpaqueRef<[string, unknown]>) =>
221+
h("li", null, [input[0], ": ", str`${input[1]}`])
222222
) as VNode[],
223223
)
224224
) as VNode[],

packages/runner/src/builder/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const schema: typeof schemaFunction = (schema) => schema;
6565
export { AuthSchema } from "./schema-lib.ts";
6666
export type {
6767
AnyCell,
68+
AnyCellWrapping,
6869
Cell,
6970
CreateCellFunction,
7071
Handler,

packages/runner/test/schema-to-ts.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "@commontools/utils/equal-ignoring-symbols";
55
import { handler, lift } from "../src/builder/module.ts";
66
import { str } from "../src/builder/built-in.ts";
77
import {
8+
type AnyCellWrapping,
89
type Frame,
910
type JSONSchema,
1011
type OpaqueRef,
@@ -760,7 +761,7 @@ describe("Schema-to-TS Type Conversion", () => {
760761

761762
// Type aliases to verify the schema inference
762763
type ExpectedInput = {
763-
name: string;
764+
name: Cell<string>;
764765
count?: number;
765766
options?: {
766767
enabled: boolean;
@@ -801,11 +802,11 @@ describe("Schema-to-TS Type Conversion", () => {
801802
type InferredInput = Parameters<typeof processRecipe>[0];
802803
type InferredOutput = ReturnType<typeof processRecipe>;
803804

804-
expectType<ExpectedInput, Schema<typeof inputSchema>>();
805+
expectType<AnyCellWrapping<ExpectedInput>, Schema<typeof inputSchema>>();
805806
expectType<ExpectedOutput, Schema<typeof outputSchema>>();
806807

807808
// Verify that the recipe function parameter matches our expected input type
808-
expectType<ExpectedInput, InferredInput>();
809+
expectType<InferredInput, ExpectedInput>();
809810

810811
// The expected output is the output schema wrapped in a single OpaqueRef.
811812
type DeepOpaqueOutput = OpaqueRef<Schema<typeof outputSchema>>;

packages/runner/test/type-utils.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { describe, it } from "@std/testing/bdd";
22
import { expect } from "@std/expect";
3-
import { isModule, isRecipe, type Opaque } from "../src/builder/types.ts";
3+
import {
4+
isModule,
5+
isRecipe,
6+
type Opaque,
7+
type OpaqueRef,
8+
} from "../src/builder/types.ts";
49
import { isWriteRedirectLink } from "../src/link-utils.ts";
510
import { LINK_V1_TAG } from "../src/sigil-types.ts";
611

@@ -9,7 +14,7 @@ describe("value type", () => {
914
const { foo, bar }: { foo: Opaque<string>; bar: Opaque<string> } = {
1015
foo: "foo",
1116
bar: "bar",
12-
} as Opaque<{
17+
} as OpaqueRef<{
1318
foo: string;
1419
bar: string;
1520
}>;

0 commit comments

Comments
 (0)