Repository: alloy-framework/alloy
Pull request #61 (2025-03-09 09:17:42)

Allow multiple refkeys for symbols
There are scenarios where a symbol needs to have multiple refkeys. For example, a library that exposes declarations needs to allow the user to set a refkey for its declarations, but also needs to be able to reference those declarations internally. Oftentimes we end up forming a reference to a declaration before the declaration has been emitted, which means we cannot know at that point what refkey to use. Such libraries must create declarations with the refkey provided by the user, and also with an internal refkey it can use for referencing its own types.

This PR also implements indentIfBreak and improves formatting of TypeScript member chain expressions when there is only one function call in the group.

It also fixes a bug with an extra comma in TypeScript ArrayExpressions without jsValues.

github-actions[bot]: <!--chronus-github-change-commenter-->
:x: There is undocummented changes. Run `chronus add` to add a changeset or [click here](https://github.com/bterlson/alloy/new/refkey-to-refkeys?filename=.chronus/changes/refkey-to-refkeys-2025-2-9-9-18-9.md&value=---%0A%23%20Change%20versionKind%20to%20one%20of%3A%20breaking%2C%20feature%2C%20fix%2C%20internal%0AchangeKind%3A%20fix%0Apackages%3A%0A%20%20-%20%22%40alloy-js%2Ftypescript%22%0A---%0A%0AAllow%20multiple%20refkeys%20for%20symbols).

**The following packages have changes but are not documented.**
 - :x:`@alloy-js/typescript`

The following packages have already been documented:
 - :white_check_mark: `@alloy-js/core`

<details>
<summary>Show changes</summary>

### `@alloy-js/core` - _breaking_ [✏️](https://github.com/bterlson/alloy/edit/refkey-to-refkeys/.chronus/changes/refkey-to-refkeys-2025-2-9-1-9-7.md?pr=/alloy-framework/alloy/pull/61)
> Symbols now allow for multiple refkeys. The `refkey` property has been removed, however the refkey prop remains on declaration components and the refkey option remains for the binder's createSymbol, so the breakage should be limited to code which interacts directly with symbols."
</details>

pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/10c53efd-91b1-4658-95de-0c3ff075a463)


<details><summary><b>@alloy-js/babel-plugin</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-plugin-jsx-dom-expressions</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin-jsx-dom-expressions@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/core</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/core@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/csharp</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/csharp@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-preset</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-preset@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/java</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/java@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/json</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/json@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/prettier-plugin-alloy</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/prettier-plugin-alloy@61
```

</p></details>
      
    

<details><summary><b>@alloy-js/typescript</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/typescript@61
```

</p></details>
      
    

_commit: <a href="https://github.com/alloy-framework/alloy/runs/38443267719"><code>dda88d4</code></a>_


Patch:
diff --git a/.chronus/changes/refkey-to-refkeys-2025-2-9-1-19-10.md b/.chronus/changes/refkey-to-refkeys-2025-2-9-1-19-10.md
new file mode 100644
index 0000000..95cefad
--- /dev/null
+++ b/.chronus/changes/refkey-to-refkeys-2025-2-9-1-19-10.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/typescript"
+---
+
+Add support for providing multiple refkeys for a single declaration.
\ No newline at end of file
diff --git a/.chronus/changes/refkey-to-refkeys-2025-2-9-1-28-26.md b/.chronus/changes/refkey-to-refkeys-2025-2-9-1-28-26.md
new file mode 100644
index 0000000..701e548
--- /dev/null
+++ b/.chronus/changes/refkey-to-refkeys-2025-2-9-1-28-26.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/json"
+---
+
+Allow specifying multiple refkeys for values.
\ No newline at end of file
diff --git a/.chronus/changes/refkey-to-refkeys-2025-2-9-1-9-7.md b/.chronus/changes/refkey-to-refkeys-2025-2-9-1-9-7.md
new file mode 100644
index 0000000..3e75810
--- /dev/null
+++ b/.chronus/changes/refkey-to-refkeys-2025-2-9-1-9-7.md
@@ -0,0 +1,7 @@
+---
+changeKind: breaking
+packages:
+  - "@alloy-js/core"
+---
+
+Symbols now allow for multiple refkeys. The `refkey` property has been removed, however the refkey prop remains on declaration components and the refkey option remains for the binder's createSymbol, so the breakage should be limited to code which interacts directly with symbols."
\ No newline at end of file
diff --git a/.chronus/changes/refkey-to-refkeys-2025-2-9-17-59-54.md b/.chronus/changes/refkey-to-refkeys-2025-2-9-17-59-54.md
new file mode 100644
index 0000000..daecf8c
--- /dev/null
+++ b/.chronus/changes/refkey-to-refkeys-2025-2-9-17-59-54.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+  - "@alloy-js/typescript"
+---
+
+Fix extra comma in array expressions with children and no jsValue.
\ No newline at end of file
diff --git a/packages/core/src/binder.ts b/packages/core/src/binder.ts
index 113b922..c99d83d 100644
--- a/packages/core/src/binder.ts
+++ b/packages/core/src/binder.ts
@@ -99,9 +99,9 @@ export interface OutputSymbol {
   binder: Binder;
 
   /**
-   * A unique value that references this symbol.
+   * The unique values that reference this symbol.
    */
-  refkey: Refkey;
+  refkeys: Refkey[];
 
   /**
    * The instance members available on this symbol.
@@ -228,6 +228,7 @@ export type CreateSymbolOptions<T extends OutputSymbol = OutputSymbol> = {
   name: string;
   scope?: OutputScope;
   refkey?: Refkey;
+  refkeys?: Refkey[];
   flags?: OutputSymbolFlags;
 } & Omit<T, keyof OutputSymbol>;
 
@@ -524,10 +525,20 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
       name,
       scope = useDefaultScope(args.flags),
       refkey,
+      refkeys,
       flags = OutputSymbolFlags.None,
       ...rest
     } = args;
 
+    const allRefkeys = [];
+    if (refkey) {
+      allRefkeys.push(refkey);
+    }
+
+    if (refkeys) {
+      allRefkeys.push(...refkeys);
+    }
+
     if (!scope) {
       throw new Error(
         "No scope was provided and no scope could be found in context",
@@ -556,7 +567,7 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
       originalName: name,
       name: name,
       scope,
-      refkey,
+      refkeys: allRefkeys,
       binder,
       flags,
       ...rest,
@@ -583,7 +594,9 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
     }
 
     scope.symbols.add(symbol);
-    scope.symbolsByRefkey.set(symbol.refkey, symbol);
+    for (const refkey of allRefkeys) {
+      scope.symbolsByRefkey.set(refkey, symbol);
+    }
 
     deconflict(symbol);
     notifyRefkey(symbol);
@@ -598,9 +611,11 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
       return;
     }
 
-    const resolution = waitingDeclarations.get(symbol.refkey);
-    if (!resolution) return;
-    resolution.value = undefined;
+    for (const refkey of symbol.refkeys) {
+      const resolution = waitingDeclarations.get(refkey);
+      if (!resolution) return;
+      resolution.value = undefined;
+    }
   }
 
   function instantiateSymbolInto(source: OutputSymbol, target: OutputSymbol) {
@@ -619,7 +634,8 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
         createSymbol({
           name: sym.name,
           scope: target.instanceMemberScope!,
-          refkey: refkey(target.refkey, sym.refkey),
+          // todo: fix this or change this or something??
+          refkeys: [refkey(target.refkeys[0], sym.refkeys[0])],
           flags: sym.flags | OutputSymbolFlags.InstanceMember,
         });
       }
@@ -816,22 +832,21 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
 
   function notifyRefkey(symbol: OutputSymbol): void {
     effect(() => {
-      const refkey = symbol.refkey;
-      if (!refkey) return;
-
-      // notify those waiting for this refkey
-      knownDeclarations.set(refkey, symbol);
-      if (waitingDeclarations.has(refkey)) {
-        const signal = waitingDeclarations.get(refkey)!;
-        signal.value = symbol;
-      }
+      for (const refkey of symbol.refkeys) {
+        // notify those waiting for this refkey
+        knownDeclarations.set(refkey, symbol);
+        if (waitingDeclarations.has(refkey)) {
+          const signal = waitingDeclarations.get(refkey)!;
+          signal.value = symbol;
+        }
 
-      // notify those waiting for this symbol name
-      const waitingScope = waitingSymbolNames.get(symbol.scope);
-      if (waitingScope) {
-        const waitingName = waitingScope.get(symbol.name);
-        if (waitingName) {
-          waitingName.value = symbol;
+        // notify those waiting for this symbol name
+        const waitingScope = waitingSymbolNames.get(symbol.scope);
+        if (waitingScope) {
+          const waitingName = waitingScope.get(symbol.name);
+          if (waitingName) {
+            waitingName.value = symbol;
+          }
         }
       }
     });
diff --git a/packages/core/src/jsx-runtime.ts b/packages/core/src/jsx-runtime.ts
index 0dd9d15..a14483b 100644
--- a/packages/core/src/jsx-runtime.ts
+++ b/packages/core/src/jsx-runtime.ts
@@ -304,6 +304,7 @@ export namespace JSX {
     literalline: {};
     lbr: {};
     indent: { children: Children };
+    indentIfBreak: { children: Children; groupId: symbol; negate?: boolean };
     fill: { children: Children };
     breakParent: {};
     ifBreak: { children: Children; flatContents?: Children; groupId?: symbol };
@@ -357,6 +358,8 @@ export function taggedComponent<TProps = Props>(
 export const intrinsicElementKey = Symbol();
 
 export type IndentIntrinsicElement = IntrinsicElementBase<"indent">;
+export type IndentIfBreakIntrinsicElement =
+  IntrinsicElementBase<"indentIfBreak">;
 export type BrIntrinsicElement = IntrinsicElementBase<"br">;
 export type LineIntrinsicElement = IntrinsicElementBase<"line">;
 export type HbrIntrinsicElement = IntrinsicElementBase<"hbr">;
@@ -379,6 +382,7 @@ export type IfBreakIntrinsicElement = IntrinsicElementBase<"ifBreak">;
 
 export type IntrinsicElement =
   | IndentIntrinsicElement
+  | IndentIfBreakIntrinsicElement
   | BrIntrinsicElement
   | LineIntrinsicElement
   | HbrIntrinsicElement
diff --git a/packages/core/src/render.ts b/packages/core/src/render.ts
index fce2f89..5e1c918 100644
--- a/packages/core/src/render.ts
+++ b/packages/core/src/render.ts
@@ -32,6 +32,7 @@ const {
     group,
     hardline,
     indent,
+    indentIfBreak,
     line,
     lineSuffix,
     lineSuffixBoundary,
@@ -348,6 +349,19 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
       switch (child.name) {
         case "indent":
           return formatHookWithChildren(indent);
+        case "indentIfBreak":
+          node.push(
+            createRenderTreeHook(newNode, {
+              print(tree, print) {
+                return indentIfBreak(print(tree), {
+                  groupId: child.props.groupId,
+                  negate: child.props.negate,
+                });
+              },
+            }),
+          );
+          renderWorker(newNode, child.props.children);
+          return;
         case "fill":
           return formatHookWithChildren(fill as any);
         case "group":
diff --git a/packages/json/src/components/JsonArray.tsx b/packages/json/src/components/JsonArray.tsx
index d1e3e59..b0d215e 100644
--- a/packages/json/src/components/JsonArray.tsx
+++ b/packages/json/src/components/JsonArray.tsx
@@ -19,6 +19,12 @@ export interface JsonArrayPropsBase {
    * elsewhere via this refkey.
    **/
   refkey?: Refkey;
+
+  /**
+   * The refkeys for the JSON array. When provided, this array can be referenced
+   * elsewhere via any of these refkeys.
+   **/
+  refkeys?: Refkey[];
 }
 
 /**
@@ -61,7 +67,10 @@ export function JsonArray(props: JsonArrayProps) {
   const binder = memberSymbol.binder;
   binder.addStaticMembersToSymbol(memberSymbol);
   if (props.refkey) {
-    memberSymbol.refkey = props.refkey;
+    memberSymbol.refkeys = [
+      ...(props.refkey ? [props.refkey] : []),
+      ...(props.refkeys ?? []),
+    ];
   }
 
   if (!("jsValue" in props)) {
diff --git a/packages/json/src/components/JsonObject.tsx b/packages/json/src/components/JsonObject.tsx
index 5433d81..c052d09 100644
--- a/packages/json/src/components/JsonObject.tsx
+++ b/packages/json/src/components/JsonObject.tsx
@@ -15,10 +15,17 @@ import { createJsonOutputSymbol } from "../symbols/json-symbol.js";
 import { JsonValue } from "./JsonValue.jsx";
 
 export interface JsonObjectPropsBase {
-  /** The refkey for the JSON array. When provided, this value can be referenced
+  /** The refkey for the JSON object. When provided, this value can be referenced
    * elsewhere via this refkey.
    **/
   refkey?: Refkey;
+
+  /**
+   * The refkeys for the JSON object. When provided, this value can be
+   * referenced elsewhere via any of these refkeys.
+   **/
+  refkeys?: Refkey[];
+
   style?: { concise?: boolean };
 }
 
@@ -65,7 +72,10 @@ export function JsonObject(props: JsonObjectProps) {
   const binder = memberSymbol.binder;
   binder.addStaticMembersToSymbol(memberSymbol);
   if (props.refkey) {
-    memberSymbol.refkey = props.refkey;
+    memberSymbol.refkeys = [
+      ...(props.refkey ? [props.refkey] : []),
+      ...(props.refkeys ?? []),
+    ];
   }
 
   if (!("jsValue" in props)) {
diff --git a/packages/json/src/components/JsonValue.tsx b/packages/json/src/components/JsonValue.tsx
index d66d643..08a2fa1 100644
--- a/packages/json/src/components/JsonValue.tsx
+++ b/packages/json/src/components/JsonValue.tsx
@@ -9,6 +9,12 @@ export interface JsonValueProps {
    **/
   refkey?: Refkey;
 
+  /**
+   * The refkeys for the JSON value. When provided, these values can be
+   * referenced elsewhere via any of these refkeys.
+   **/
+  refkeys?: Refkey[];
+
   /**
    * The JS value to serialize to JSON. When provided, thi
    */
@@ -23,18 +29,22 @@ export interface JsonValueProps {
  * appropriate.
  */
 export function JsonValue(props: JsonValueProps) {
+  const refkeys = [
+    ...(props.refkey ? [props.refkey] : []),
+    ...(props.refkeys ?? []),
+  ];
   if (props.jsValue === null) {
-    setMemberRefkey(props.refkey);
+    setMemberRefkey(refkeys);
     return "null";
   }
 
   if (typeof props.jsValue === "number" || typeof props.jsValue === "boolean") {
-    setMemberRefkey(props.refkey);
+    setMemberRefkey(refkeys);
     return String(props.jsValue);
   }
 
   if (typeof props.jsValue === "string") {
-    setMemberRefkey(props.refkey);
+    setMemberRefkey(refkeys);
     return `"${props.jsValue}"`;
   }
 
@@ -49,11 +59,12 @@ export function JsonValue(props: JsonValueProps) {
   throw new Error("Cannot emit js value with type of " + typeof props.jsValue);
 }
 
-function setMemberRefkey(key?: Refkey) {
-  if (!key) return;
+function setMemberRefkey(keys: Refkey[]) {
+  if (keys.length === 0) return;
   const member = useMemberDeclaration();
   if (!member) {
     throw new Error("Missing member declaration.");
   }
-  member.refkey = key;
+
+  member.refkeys = keys;
 }
diff --git a/packages/typescript/src/components/ArrayExpression.tsx b/packages/typescript/src/components/ArrayExpression.tsx
index 8e7f3fe..d27d91d 100644
--- a/packages/typescript/src/components/ArrayExpression.tsx
+++ b/packages/typescript/src/components/ArrayExpression.tsx
@@ -16,7 +16,11 @@ export function ArrayExpression(props: ArrayExpressionProps) {
         </For>
         {props.children && (
           <>
-            ,<br />
+            {props.jsValue && props.jsValue.length > 0 && (
+              <>
+                ,<br />
+              </>
+            )}
             {props.children}
           </>
         )}
diff --git a/packages/typescript/src/components/Declaration.tsx b/packages/typescript/src/components/Declaration.tsx
index 10186a4..ee9959b 100644
--- a/packages/typescript/src/components/Declaration.tsx
+++ b/packages/typescript/src/components/Declaration.tsx
@@ -3,7 +3,6 @@ import {
   Declaration as CoreDeclaration,
   MemberScope,
   OutputSymbolFlags,
-  refkey,
   Refkey,
 } from "@alloy-js/core";
 import { TypeScriptElements, useTSNamePolicy } from "../name-policy.js";
@@ -29,6 +28,11 @@ export interface BaseDeclarationProps {
    */
   refkey?: Refkey;
 
+  /**
+   * Multiple unique keys for this declaration.
+   */
+  refkeys?: Refkey[];
+
   /**
    * Whether to export this declaration from the module.
    */
@@ -84,7 +88,8 @@ export function Declaration(props: DeclarationProps) {
 
   const sym = createTSSymbol({
     name: namePolicy.getName(props.name, props.nameKind),
-    refkey: props.refkey ?? refkey(props.name),
+    refkey: props.refkey,
+    refkeys: props.refkeys,
     export: props.export,
     default: props.default,
     flags: props.flags,
diff --git a/packages/typescript/src/components/EnumDeclaration.tsx b/packages/typescript/src/components/EnumDeclaration.tsx
index ea9ff1d..791c18e 100644
--- a/packages/typescript/src/components/EnumDeclaration.tsx
+++ b/packages/typescript/src/components/EnumDeclaration.tsx
@@ -6,7 +6,6 @@ import {
   MemberScope,
   Name,
   OutputSymbolFlags,
-  refkey,
   useBinder,
 } from "@alloy-js/core";
 import { useTSNamePolicy } from "../name-policy.js";
@@ -31,7 +30,8 @@ export function EnumDeclaration(props: EnumDeclarationProps) {
     binder,
     scope,
     name: name,
-    refkey: props.refkey ?? refkey(name),
+    refkey: props.refkey,
+    refkeys: props.refkeys,
     default: props.default,
     export: props.export,
     flags: OutputSymbolFlags.StaticMemberContainer,
diff --git a/packages/typescript/src/components/ExportStatement.tsx b/packages/typescript/src/components/ExportStatement.tsx
index 9eea5b7..5a20419 100644
--- a/packages/typescript/src/components/ExportStatement.tsx
+++ b/packages/typescript/src/components/ExportStatement.tsx
@@ -12,7 +12,9 @@ export function ExportStatement(props: ExportStatementProps) {
   if (props.star) {
     const module = useSourceFile();
     for (const symbol of props.from!.symbols.values()) {
-      module.scope.exportedSymbols.set(symbol.refkey, symbol as TSOutputSymbol);
+      for (const refkey of symbol.refkeys) {
+        module.scope.exportedSymbols.set(refkey, symbol as TSOutputSymbol);
+      }
     }
     return (
       <>
diff --git a/packages/typescript/src/components/FunctionDeclaration.tsx b/packages/typescript/src/components/FunctionDeclaration.tsx
index 6bf51ab..798f7ec 100644
--- a/packages/typescript/src/components/FunctionDeclaration.tsx
+++ b/packages/typescript/src/components/FunctionDeclaration.tsx
@@ -24,7 +24,8 @@ import { BaseDeclarationProps, Declaration } from "./Declaration.js";
 
 export interface ParameterDescriptor {
   type?: Children;
-  refkey: Refkey;
+  refkey?: Refkey;
+  refkeys?: Refkey[];
   optional?: boolean;
 }
 
@@ -117,7 +118,7 @@ function normalizeAndDeclareParameters(
         tsFlags: flags,
       });
 
-      return { refkey: symbol.refkey, symbol };
+      return { refkeys: symbol.refkeys, symbol };
     });
   }
 
@@ -136,6 +137,7 @@ function normalizeAndDeclareParameters(
         flags & TSSymbolFlags.TypeSymbol ? "type" : "parameter",
       ),
       refkey: descriptor.refkey,
+      refkeys: descriptor.refkeys,
       tsFlags: flags,
     });
     descriptor.symbol = symbol;
diff --git a/packages/typescript/src/components/MemberChainExpression.tsx b/packages/typescript/src/components/MemberChainExpression.tsx
index 51eabd9..d69bfd0 100644
--- a/packages/typescript/src/components/MemberChainExpression.tsx
+++ b/packages/typescript/src/components/MemberChainExpression.tsx
@@ -43,11 +43,23 @@ export function MemberChainExpression(props: MemberChainExpressionProps) {
 
     return [chunks[0], chunks.slice(1)];
   });
+  const groupId = Symbol();
 
   return (
     <group>
-      <List joiner="." children={chunks.value[0]} />
-      <Show when={chunks.value[1].length > 0}>
+      <group id={groupId}>
+        <List joiner="." children={chunks.value[0]} />
+      </group>
+      <Show when={chunks.value[1].length === 1}>
+        <For each={chunks.value[1]} softline>
+          {(chunk) => (
+            <>
+              .<List joiner="." children={chunk} />
+            </>
+          )}
+        </For>
+      </Show>
+      <Show when={chunks.value[1].length > 1}>
         <indent>
           <For each={chunks.value[1]} softline>
             {(chunk) => (
diff --git a/packages/typescript/src/components/VarDeclaration.tsx b/packages/typescript/src/components/VarDeclaration.tsx
index 1d16a52..1ffeec9 100644
--- a/packages/typescript/src/components/VarDeclaration.tsx
+++ b/packages/typescript/src/components/VarDeclaration.tsx
@@ -4,7 +4,6 @@ import {
   Declaration as CoreDeclaration,
   createAssignmentContext,
   Name,
-  refkey,
 } from "@alloy-js/core";
 import { useTSNamePolicy } from "../name-policy.js";
 import { createTSSymbol } from "../symbols/index.js";
@@ -27,7 +26,8 @@ export function VarDeclaration(props: VarDeclarationProps) {
   const name = useTSNamePolicy().getName(props.name, "variable");
   const sym = createTSSymbol({
     name: name,
-    refkey: props.refkey ?? refkey(name),
+    refkey: props.refkey,
+    refkeys: props.refkeys,
     default: props.default,
     export: props.export,
   });
diff --git a/packages/typescript/src/symbols/reference.ts b/packages/typescript/src/symbols/reference.ts
index d84af89..693a3b9 100644
--- a/packages/typescript/src/symbols/reference.ts
+++ b/packages/typescript/src/symbols/reference.ts
@@ -53,10 +53,13 @@ export function ref(refkey: Refkey): () => string {
 
       // find public dependency
       for (const module of sourcePackage.exportedSymbols.values()) {
-        if (module.exportedSymbols.has(importSymbol.refkey)) {
-          localSymbol = untrack(() =>
-            sourceFile!.scope.addImport(importSymbol, module),
-          );
+        for (const refkey of importSymbol.refkeys) {
+          if (module.exportedSymbols.has(refkey)) {
+            localSymbol = untrack(() =>
+              sourceFile!.scope.addImport(importSymbol, module),
+            );
+            break;
+          }
         }
       }
 
diff --git a/packages/typescript/src/symbols/ts-output-symbol.ts b/packages/typescript/src/symbols/ts-output-symbol.ts
index 71014c2..5c14879 100644
--- a/packages/typescript/src/symbols/ts-output-symbol.ts
+++ b/packages/typescript/src/symbols/ts-output-symbol.ts
@@ -26,7 +26,8 @@ export interface TSOutputSymbol extends OutputSymbol {
 
 export interface createTsSymbolOptions {
   name: string;
-  refkey: Refkey;
+  refkey?: Refkey;
+  refkeys?: Refkey[];
   binder?: Binder;
   scope?: TSOutputScope;
   export?: boolean;
@@ -49,6 +50,7 @@ export function createTSSymbol(options: createTsSymbolOptions): TSOutputSymbol {
     name: options.name,
     scope,
     refkey: options.refkey,
+    refkeys: options.refkeys,
     export: !!options.export,
     default: !!options.default,
     flags: options.flags ?? OutputSymbolFlags.None,
@@ -56,7 +58,9 @@ export function createTSSymbol(options: createTsSymbolOptions): TSOutputSymbol {
   });
 
   if (options.export && scope.kind === "module") {
-    scope.exportedSymbols.set(sym.refkey, sym);
+    for (const refkey of sym.refkeys) {
+      scope.exportedSymbols.set(refkey, sym);
+    }
   }
 
   return sym;


Test patch:
diff --git a/packages/core/test/symbols.test.ts b/packages/core/test/symbols.test.ts
index 21afdb0..9fe93ab 100644
--- a/packages/core/test/symbols.test.ts
+++ b/packages/core/test/symbols.test.ts
@@ -158,7 +158,7 @@ describe("static members", () => {
     const resolution = binder.resolveDeclarationByKey(
       root,
       undefined,
-      staticSym.refkey,
+      staticSym.refkeys[0],
     );
     expect(resolution.value).toBeDefined();
     const { commonScope, pathUp, pathDown, targetDeclaration, memberPath } =
@@ -202,7 +202,7 @@ describe("static members", () => {
     const resolution = binder.resolveDeclarationByKey(
       root,
       undefined,
-      nested_static.refkey,
+      nested_static.refkeys[0],
     );
     expect(resolution.value).toBeDefined();
     const { commonScope, pathUp, pathDown, targetDeclaration, memberPath } =
@@ -284,7 +284,7 @@ describe("instance members", () => {
     const resolution = binder.resolveDeclarationByKey(
       undefined,
       rootSym.instanceMemberScope!,
-      instance.refkey,
+      instance.refkeys[0],
     );
     expect(resolution.value).toBeDefined();
     const { commonScope, pathUp, pathDown, targetDeclaration, memberPath } =
@@ -318,7 +318,7 @@ describe("instance members", () => {
     });
 
     expect(() =>
-      binder.resolveDeclarationByKey(root, undefined, instance.refkey),
+      binder.resolveDeclarationByKey(root, undefined, instance.refkeys[0]),
     ).toThrow(/Cannot resolve member symbols/);
   });
 });
@@ -349,7 +349,10 @@ describe("instantiating members", () => {
       instantiation.flags & OutputSymbolFlags.InstanceMemberContainer,
     ).toBeTruthy();
     expect(instantiation.instanceMemberScope).toBeDefined();
-    const expectedRefkey = refkey(instantiation.refkey, instance.refkey);
+    const expectedRefkey = refkey(
+      instantiation.refkeys[0],
+      instance.refkeys[0],
+    );
     expect(
       instantiation.instanceMemberScope!.symbolsByRefkey.get(expectedRefkey),
     ).toBeDefined();
@@ -380,7 +383,10 @@ describe("instantiating members", () => {
       instantiation.flags & OutputSymbolFlags.InstanceMemberContainer,
     ).toBeTruthy();
     expect(instantiation.instanceMemberScope).toBeDefined();
-    const expectedRefkey = refkey(instantiation.refkey, instance.refkey);
+    const expectedRefkey = refkey(
+      instantiation.refkeys[0],
+      instance.refkeys[0],
+    );
     expect(
       instantiation.instanceMemberScope!.symbolsByRefkey.get(expectedRefkey),
     ).toBeDefined();
@@ -393,7 +399,7 @@ describe("instantiating members", () => {
       flags: OutputSymbolFlags.InstanceMember,
     });
     const newExpectedRefkey = refkey(
-      instantiation.refkey,
+      instantiation.refkeys[0],
       newInstanceMemberRefkey,
     );
     expect(
@@ -558,7 +564,7 @@ describe("refkey resolution", () => {
 
     expect(resolvedSym.value).toBe(undefined);
 
-    sym.refkey = key;
+    sym.refkeys[0] = key;
     expect(resolvedSym.value?.targetDeclaration).toBe(sym);
   });
 });
@@ -581,7 +587,7 @@ describe("Deleting symbols", () => {
 
     expect(resolvedSym.value).toBe(undefined);
 
-    sym.refkey = key;
+    sym.refkeys[0] = key;
     expect(resolvedSym.value?.targetDeclaration).toBe(sym);
 
     binder.deleteSymbol(sym);
diff --git a/packages/typescript/test/array.test.tsx b/packages/typescript/test/array.test.tsx
index 6210484..82a7ec8 100644
--- a/packages/typescript/test/array.test.tsx
+++ b/packages/typescript/test/array.test.tsx
@@ -33,3 +33,15 @@ it("accepts children", () => {
     [1, 2, 3, [4, 5, 6]]
   `);
 });
+
+it("accepts only children", () => {
+  expect(
+    toSourceText(
+      <ArrayExpression>
+        <ArrayExpression jsValue={[4, 5, 6]} />
+      </ArrayExpression>,
+    ),
+  ).toBe(d`
+    [[4, 5, 6]]
+  `);
+});
diff --git a/packages/typescript/test/imports.test.tsx b/packages/typescript/test/imports.test.tsx
index f3240fd..0b5aec0 100644
--- a/packages/typescript/test/imports.test.tsx
+++ b/packages/typescript/test/imports.test.tsx
@@ -46,7 +46,7 @@ it("works with named imports", () => {
   const res = render(
     <Output>
       <ts.SourceFile path="test1.ts">
-        <ts.FunctionDeclaration export name="test" />
+        <ts.FunctionDeclaration export name="test" refkey={refkey("test")} />
       </ts.SourceFile>
 
       <ts.SourceFile path="test2.ts">
@@ -71,9 +71,14 @@ it("works with default and named imports", () => {
   const res = render(
     <Output>
       <ts.SourceFile path="test1.ts">
-        <ts.FunctionDeclaration export default name="test1" />
+        <ts.FunctionDeclaration
+          export
+          default
+          name="test1"
+          refkey={refkey("test1")}
+        />
         <hbr />
-        <ts.FunctionDeclaration export name="test2" />
+        <ts.FunctionDeclaration export name="test2" refkey={refkey("test2")} />
       </ts.SourceFile>
 
       <ts.SourceFile path="test2.ts">
@@ -101,9 +106,14 @@ it("works with default and named imports and name conflicts", () => {
   const res = render(
     <Output nameConflictResolver={tsNameConflictResolver}>
       <ts.SourceFile path="test1.ts">
-        <ts.FunctionDeclaration export default name="test1" />
+        <ts.FunctionDeclaration
+          export
+          default
+          name="test1"
+          refkey={refkey("test1")}
+        />
         <br />
-        <ts.FunctionDeclaration export name="test2" />
+        <ts.FunctionDeclaration export name="test2" refkey={refkey("test2")} />
       </ts.SourceFile>
 
       <ts.SourceFile path="test2.ts">
@@ -165,9 +175,14 @@ it("works with default and named imports and name conflicts and references in ne
   const res = render(
     <Output nameConflictResolver={tsNameConflictResolver}>
       <ts.SourceFile path="test1.ts">
-        <ts.FunctionDeclaration export default name="test1" />
+        <ts.FunctionDeclaration
+          export
+          default
+          name="test1"
+          refkey={refkey("test1")}
+        />
         <hbr />
-        <ts.FunctionDeclaration export name="test2" />
+        <ts.FunctionDeclaration export name="test2" refkey={refkey("test2")} />
       </ts.SourceFile>
 
       <ts.SourceFile path="test2.ts">
@@ -237,7 +252,7 @@ it("works with imports from different directories", () => {
     <Output>
       <SourceDirectory path="src">
         <ts.SourceFile path="test1.ts">
-          <ts.FunctionDeclaration export name="test" />
+          <ts.FunctionDeclaration export name="test" refkey={refkey("test")} />
           <hbr />
           const v = <Reference refkey={refkey("test2")} />;
         </ts.SourceFile>
@@ -245,7 +260,7 @@ it("works with imports from different directories", () => {
 
       <ts.SourceFile path="test2.ts">
         const v = <Reference refkey={refkey("test")} />;<hbr />
-        <ts.FunctionDeclaration export name="test2" />
+        <ts.FunctionDeclaration export name="test2" refkey={refkey("test2")} />
       </ts.SourceFile>
     </Output>,
   );
@@ -271,7 +286,7 @@ it("handles conflicts with local declarations", () => {
     <Output nameConflictResolver={tsNameConflictResolver}>
       <SourceDirectory path="src">
         <ts.SourceFile path="test1.ts">
-          <ts.FunctionDeclaration export name="test" />
+          <ts.FunctionDeclaration export name="test" refkey={refkey("test")} />
           <hbr />
           const v = <Reference refkey={refkey("test2")} />;
         </ts.SourceFile>
diff --git a/packages/typescript/test/interface.test.tsx b/packages/typescript/test/interface.test.tsx
index 5c3065b..83966ef 100644
--- a/packages/typescript/test/interface.test.tsx
+++ b/packages/typescript/test/interface.test.tsx
@@ -35,7 +35,7 @@ it("creates extends", () => {
 
 it("can create members", () => {
   const res = toSourceText(
-    <ts.InterfaceDeclaration name="Foo">
+    <ts.InterfaceDeclaration name="Foo" refkey={refkey("Foo")}>
       <StatementList>
         <ts.InterfaceMember name="member" type="string" />
         <ts.InterfaceMember
@@ -58,7 +58,7 @@ it("can create members", () => {
 
 it("can create optional members", () => {
   const res = toSourceText(
-    <ts.InterfaceDeclaration name="Foo">
+    <ts.InterfaceDeclaration name="Foo" refkey={refkey("Foo")}>
       <StatementList>
         <ts.InterfaceMember name="member" type="string" />
         <ts.InterfaceMember
@@ -82,7 +82,7 @@ it("can create optional members", () => {
 
 it("can create readonly members", () => {
   const res = toSourceText(
-    <ts.InterfaceDeclaration name="Foo">
+    <ts.InterfaceDeclaration name="Foo" refkey={refkey("Foo")}>
       <StatementList>
         <ts.InterfaceMember readonly name="member" type="string" />
         <ts.InterfaceMember
diff --git a/packages/typescript/test/member-chain.test.tsx b/packages/typescript/test/member-chain.test.tsx
index 11982d8..0de0b0c 100644
--- a/packages/typescript/test/member-chain.test.tsx
+++ b/packages/typescript/test/member-chain.test.tsx
@@ -46,6 +46,44 @@ it("works with args which break", () => {
   `);
 });
 
+it("works with args when it doesn't break", () => {
+  expect(
+    toSourceText(
+      <MemberChainExpression>
+        <>z</>
+        <FunctionCallExpression
+          target="object"
+          args={[<ValueExpression jsValue={{ a: 1, b: 2 }} />]}
+        />
+      </MemberChainExpression>,
+    ),
+  ).toBe(d`
+    z.object({
+      a: 1,
+      b: 2,
+    })
+  `);
+
+  expect(
+    toSourceText(
+      <MemberChainExpression>
+        <>z</>
+        <>z2</>
+        <>z3</>
+        <FunctionCallExpression
+          target="object"
+          args={[<ValueExpression jsValue={{ a: 1, b: 2 }} />]}
+        />
+      </MemberChainExpression>,
+    ),
+  ).toBe(d`
+    z.z2.z3.object({
+      a: 1,
+      b: 2,
+    })
+  `);
+});
+
 it("works with nested call member chains", () => {
   expect(
     toSourceText(
diff --git a/packages/typescript/test/package-external.test.tsx b/packages/typescript/test/package-external.test.tsx
index 74acef9..5a14567 100644
--- a/packages/typescript/test/package-external.test.tsx
+++ b/packages/typescript/test/package-external.test.tsx
@@ -13,13 +13,20 @@ it("imports external packages", () => {
         version="1.0.0"
       >
         <ts.SourceFile path="greetings.ts">
-          <ts.FunctionDeclaration name="getGreeting">
+          <ts.FunctionDeclaration
+            name="getGreeting"
+            refkey={refkey("getGreeting")}
+          >
             return "Hello world!";
           </ts.FunctionDeclaration>
         </ts.SourceFile>
 
         <ts.SourceFile path="logGreetings.ts">
-          <ts.FunctionDeclaration export name="printGreeting">
+          <ts.FunctionDeclaration
+            export
+            name="printGreeting"
+            refkey={refkey("printGreeting")}
+          >
             console.log("Hello world!");
           </ts.FunctionDeclaration>
         </ts.SourceFile>
diff --git a/packages/typescript/test/package.test.tsx b/packages/typescript/test/package.test.tsx
index 2b91a4f..8ec1169 100644
--- a/packages/typescript/test/package.test.tsx
+++ b/packages/typescript/test/package.test.tsx
@@ -10,7 +10,10 @@ it("exports source files", () => {
     <Output>
       <PackageDirectory name="greeting-js" path="." version="1.0.0">
         <ts.SourceFile path="greeting.ts">
-          <ts.FunctionDeclaration name="getGreeting">
+          <ts.FunctionDeclaration
+            name="getGreeting"
+            refkey={refkey("getGreeting")}
+          >
             return "Hello world!";
           </ts.FunctionDeclaration>
         </ts.SourceFile>
@@ -20,7 +23,11 @@ it("exports source files", () => {
             {refkey("getGreeting")}()
           </ts.VarDeclaration>
           ;<hbr />
-          <ts.FunctionDeclaration export name="printGreeting">
+          <ts.FunctionDeclaration
+            export
+            name="printGreeting"
+            refkey={refkey("printGreeting")}
+          >
             console.log(greeting);
           </ts.FunctionDeclaration>
         </ts.SourceFile>