Repository: alloy-framework/alloy
Pull request #91: Function and Arrow function expression components
Author: login: timotheeguerin
Created: 2025-04-09T20:17:13Z | Merged: 2025-04-09T20:46:06Z

fix #88 

Linked issue #88: TypeScript needs Function Expression forms
Both normal function expressions and arrow functions.

Diff:
diff --git a/.chronus/changes/function-expressions-2025-3-9-20-17-39.md b/.chronus/changes/function-expressions-2025-3-9-20-17-39.md
new file mode 100644
index 000000000..e1825a646
--- /dev/null
+++ b/.chronus/changes/function-expressions-2025-3-9-20-17-39.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: feature
+packages:
+  - "@alloy-js/typescript"
+---
+
+Add new `<FunctionExpression />` and `<ArrowFunctionExpression />` components supporting the various formm of function expressions
diff --git a/.chronus/changes/function-expressions-2025-3-9-20-41-8.md b/.chronus/changes/function-expressions-2025-3-9-20-41-8.md
new file mode 100644
index 000000000..9c33caa54
--- /dev/null
+++ b/.chronus/changes/function-expressions-2025-3-9-20-41-8.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: fix
+packages:
+  - "@alloy-js/core"
+---
+
+`taggedComponent` return type updated to make tag as a required property
diff --git a/packages/core/src/jsx-runtime.ts b/packages/core/src/jsx-runtime.ts
index 9e2823ea3..eadc28fb3 100644
--- a/packages/core/src/jsx-runtime.ts
+++ b/packages/core/src/jsx-runtime.ts
@@ -462,9 +462,9 @@ export function createComponent<TProps extends Props = Props>(
 export function taggedComponent<TProps = Props>(
   tag: symbol,
   component: Component<TProps>,
-): Component<TProps> {
+): Component<TProps> & Required<Pick<Component<TProps>, "tag">> {
   component.tag = tag;
-  return component;
+  return component as any;
 }
 
 export const intrinsicElementKey = Symbol();
diff --git a/packages/typescript/src/components/ArrowFunction.tsx b/packages/typescript/src/components/ArrowFunction.tsx
new file mode 100644
index 000000000..3b933286f
--- /dev/null
+++ b/packages/typescript/src/components/ArrowFunction.tsx
@@ -0,0 +1,89 @@
+import {
+  childrenArray,
+  findKeyedChild,
+  findUnkeyedChildren,
+  Scope,
+} from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+import { getCallSignatureProps } from "../utils.js";
+import { CallSignature, CallSignatureProps } from "./CallSignature.jsx";
+import { Declaration } from "./Declaration.jsx";
+import {
+  FunctionBody,
+  FunctionParameters,
+  FunctionTypeParameters,
+  getReturnType,
+} from "./FunctionBase.jsx";
+
+export interface ArrowFunctionProps extends CallSignatureProps {
+  async?: boolean;
+  children?: Children;
+}
+
+/**
+ * A TypeScript arrow function expression.
+ *
+ * @example
+ * ```tsx
+ * <FunctionExpression async parameters={[{ name: "a", type: "number" }, { name: "b", type: "number" }]}>
+ *   return a + b;
+ * <FunctionExpression>
+ * ```
+ * renders to
+ * ```ts
+ * async (a, b) => { return a + b; }
+ * ```
+ *
+ * @remarks
+ *
+ * Providing parameters and type parameters can be accomplished in one of three
+ * ways:
+ *
+ * 1. As an array of {@link ParameterDescriptor}s or
+ *    {@link TypeParameterDescriptor}s.
+ * 2. As raw content via the `parametersChildren` or `typeParametersChildren`
+ *    props.
+ * 3. As a child of this component via the
+ *    {@link (ArrowFunction:namespace).Parameters} or
+ *    {@link (ArrowFunction:namespace).TypeParameters} components.
+ */
+export function ArrowFunction(props: ArrowFunctionProps) {
+  const children = childrenArray(() => props.children);
+  const typeParametersChildren =
+    findKeyedChild(children, FunctionTypeParameters.tag) ?? undefined;
+  const parametersChildren =
+    findKeyedChild(children, FunctionParameters.tag) ?? undefined;
+  const bodyChildren = findKeyedChild(children, FunctionBody.tag) ?? undefined;
+  const filteredChildren = findUnkeyedChildren(children);
+  const returnType = getReturnType(props.returnType, { async: props.async });
+
+  const sBody = bodyChildren ?? (
+    <ArrowFunction.Body>
+      {filteredChildren}
+    </ArrowFunction.Body>
+  );
+
+  const asyncKwd = props.async ? "async " : "";
+
+  const callSignatureProps = getCallSignatureProps(props, {
+    parametersChildren,
+    typeParametersChildren,
+  });
+
+  return (
+    <>
+      <Declaration {...props} nameKind="function">
+        {asyncKwd}
+        <Scope kind="function">
+          <CallSignature {...callSignatureProps} returnType={returnType} />
+          {" => "}
+          {sBody}
+        </Scope>
+      </Declaration>
+    </>
+  );
+}
+
+ArrowFunction.TypeParameters = FunctionTypeParameters;
+ArrowFunction.Parameters = FunctionParameters;
+ArrowFunction.Body = FunctionBody;
diff --git a/packages/typescript/src/components/FunctionBase.tsx b/packages/typescript/src/components/FunctionBase.tsx
new file mode 100644
index 000000000..6bd365ad9
--- /dev/null
+++ b/packages/typescript/src/components/FunctionBase.tsx
@@ -0,0 +1,195 @@
+import {
+  Block,
+  code,
+  For,
+  Indent,
+  onCleanup,
+  Show,
+  taggedComponent,
+} from "@alloy-js/core";
+import type { Children } from "@alloy-js/core/jsx-runtime";
+import { useTSNamePolicy } from "../name-policy.js";
+import type { ParameterDescriptor } from "../parameter-descriptor.js";
+import { TypeParameterDescriptor } from "../parameter-descriptor.js";
+import {
+  createTSSymbol,
+  TSOutputSymbol,
+  TSSymbolFlags,
+} from "../symbols/index.js";
+
+const functionParametersTag = Symbol();
+const functionTypeParametersTag = Symbol();
+const functionBodyTag = Symbol();
+
+export interface FunctionBodyProps {
+  children?: Children;
+}
+
+export interface FunctionParametersProps {
+  parameters?: ParameterDescriptor[] | string[];
+  children?: Children;
+}
+
+export const FunctionBody = taggedComponent(
+  functionBodyTag,
+  function Body(props: FunctionBodyProps) {
+    return <Block>{props.children}</Block>;
+  },
+);
+
+export interface FunctionTypeParametersProps {
+  parameters?: ParameterDescriptor[] | string[];
+  children?: Children;
+}
+
+export const FunctionParameters = taggedComponent(
+  functionParametersTag,
+  function Parameters(props: FunctionParametersProps) {
+    if (props.children) {
+      return props.children;
+    }
+
+    const parameters = normalizeAndDeclareParameters(props.parameters ?? []);
+    return (
+      <group>
+        <Indent softline trailingBreak>
+          <For each={parameters} comma line>
+            {(param) => parameter(param)}
+          </For>
+          <ifBreak>,</ifBreak>
+        </Indent>
+      </group>
+    );
+  },
+);
+
+function parameter(param: DeclaredParameterDescriptor) {
+  return (
+    <group>
+      {param.symbol.name}
+      <Show when={!!param.optional}>?</Show>
+      <Show when={!!param.type}>
+        <indent>: {param.type}</indent>
+      </Show>
+    </group>
+  );
+}
+
+interface DeclaredParameterDescriptor
+  extends Omit<ParameterDescriptor, "name"> {
+  symbol: TSOutputSymbol;
+}
+
+interface DeclaredTypeParameterDescriptor
+  extends Omit<TypeParameterDescriptor, "name"> {
+  symbol: TSOutputSymbol;
+}
+function normalizeAndDeclareParameters(
+  parameters: TypeParameterDescriptor[] | string[],
+  flags?: TSSymbolFlags,
+): DeclaredTypeParameterDescriptor[];
+function normalizeAndDeclareParameters(
+  parameters: ParameterDescriptor[] | string[],
+  flags?: TSSymbolFlags,
+): DeclaredParameterDescriptor[];
+function normalizeAndDeclareParameters(
+  parameters: string[],
+  flags?: TSSymbolFlags,
+): DeclaredParameterDescriptor[] | DeclaredTypeParameterDescriptor[];
+function normalizeAndDeclareParameters(
+  parameters: ParameterDescriptor[] | TypeParameterDescriptor[] | string[],
+  flags: TSSymbolFlags = TSSymbolFlags.ParameterSymbol,
+): DeclaredParameterDescriptor[] | DeclaredTypeParameterDescriptor[] {
+  const namePolicy = useTSNamePolicy();
+  if (parameters.length === 0) {
+    return [];
+  }
+  if (typeof parameters[0] === "string") {
+    return (parameters as string[]).map((paramName) => {
+      const symbol = createTSSymbol({
+        name: namePolicy.getName(
+          paramName,
+          flags & TSSymbolFlags.TypeSymbol ? "type" : "parameter",
+        ),
+        tsFlags: flags,
+      });
+
+      return { refkeys: symbol.refkeys, symbol };
+    });
+  } else {
+    return (parameters as ParameterDescriptor[]).map((param) => {
+      const symbol = createTSSymbol({
+        name: namePolicy.getName(
+          param.name,
+          flags & TSSymbolFlags.TypeSymbol ? "type" : "parameter",
+        ),
+        refkey: param.refkey,
+        tsFlags: flags,
+        metadata: param.metadata,
+      });
+
+      return {
+        ...param,
+        symbol,
+      };
+    });
+  }
+}
+
+export const FunctionTypeParameters = taggedComponent(
+  functionTypeParametersTag,
+  function TypeParameters(props: FunctionTypeParametersProps) {
+    if (props.children) {
+      return props.children;
+    }
+
+    if (!props.parameters) {
+      return undefined;
+    }
+
+    const typeParameters = normalizeAndDeclareParameters(props.parameters);
+
+    onCleanup(() => {
+      for (const param of typeParameters) {
+        param.symbol.binder.deleteSymbol(param.symbol);
+      }
+    });
+
+    return (
+      <>
+        {"<"}
+        <group>
+          <Indent softline>
+            <For each={typeParameters} comma line>
+              {(param) => typeParameter(param)}
+            </For>
+            <ifBreak>,</ifBreak>
+          </Indent>
+        </group>
+        {">"}
+      </>
+    );
+  },
+);
+
+function typeParameter(param: DeclaredTypeParameterDescriptor) {
+  return (
+    <group>
+      {param.symbol.name}
+      <Show when={!!param.extends}>
+        {" "}
+        extends
+        <indent> {param.extends}</indent>
+      </Show>
+    </group>
+  );
+}
+
+export function getReturnType(
+  returnType: Children | undefined,
+  options: { async?: boolean } = { async: false },
+) {
+  if (returnType) {
+    return options.async ? code`Promise<${returnType}>` : returnType;
+  }
+}
diff --git a/packages/typescript/src/components/FunctionDeclaration.tsx b/packages/typescript/src/components/FunctionDeclaration.tsx
index 8d75821c6..f46fa88a0 100644
--- a/packages/typescript/src/components/FunctionDeclaration.tsx
+++ b/packages/typescript/src/components/FunctionDeclaration.tsx
@@ -1,31 +1,22 @@
 import {
-  Block,
   childrenArray,
-  code,
   findKeyedChild,
   findUnkeyedChildren,
-  For,
-  Indent,
   Name,
   Prose,
   Scope,
   Show,
-  taggedComponent,
 } from "@alloy-js/core";
-import { Children, onCleanup } from "@alloy-js/core/jsx-runtime";
-import { useTSNamePolicy } from "../name-policy.js";
-import {
-  ParameterDescriptor,
-  TypeParameterDescriptor,
-} from "../parameter-descriptor.js";
-import {
-  createTSSymbol,
-  TSOutputSymbol,
-  TSSymbolFlags,
-} from "../symbols/index.js";
+import { Children } from "@alloy-js/core/jsx-runtime";
 import { getCallSignatureProps } from "../utils.js";
 import { CallSignature, CallSignatureProps } from "./CallSignature.jsx";
 import { BaseDeclarationProps, Declaration } from "./Declaration.js";
+import {
+  FunctionBody,
+  FunctionParameters,
+  FunctionTypeParameters,
+  getReturnType,
+} from "./FunctionBase.jsx";
 import { JSDoc } from "./JSDoc.jsx";
 import { JSDocParams } from "./JSDocParam.jsx";
 
@@ -36,10 +27,6 @@ export interface FunctionDeclarationProps
   children?: Children;
 }
 
-export const functionParametersTag = Symbol();
-export const functionTypeParametersTag = Symbol();
-export const functionBodyTag = Symbol();
-
 /**
  * A TypeScript function declaration.
  *
@@ -59,10 +46,10 @@ export const functionBodyTag = Symbol();
 export function FunctionDeclaration(props: FunctionDeclarationProps) {
   const children = childrenArray(() => props.children);
   const typeParametersChildren =
-    findKeyedChild(children, functionTypeParametersTag) ?? undefined;
+    findKeyedChild(children, FunctionTypeParameters.tag) ?? undefined;
   const parametersChildren =
-    findKeyedChild(children, functionParametersTag) ?? undefined;
-  const bodyChildren = findKeyedChild(children, functionBodyTag) ?? undefined;
+    findKeyedChild(children, FunctionParameters.tag) ?? undefined;
+  const bodyChildren = findKeyedChild(children, FunctionBody.tag) ?? undefined;
   const filteredChildren = findUnkeyedChildren(children);
   const returnType = getReturnType(props.returnType, { async: props.async });
 
@@ -99,175 +86,6 @@ export function FunctionDeclaration(props: FunctionDeclarationProps) {
   );
 }
 
-function getReturnType(
-  returnType: Children | undefined,
-  options: { async?: boolean } = { async: false },
-) {
-  if (returnType) {
-    return options.async ? code`Promise<${returnType}>` : returnType;
-  }
-}
-
-interface DeclaredParameterDescriptor
-  extends Omit<ParameterDescriptor, "name"> {
-  symbol: TSOutputSymbol;
-}
-
-interface DeclaredTypeParameterDescriptor
-  extends Omit<TypeParameterDescriptor, "name"> {
-  symbol: TSOutputSymbol;
-}
-function normalizeAndDeclareParameters(
-  parameters: TypeParameterDescriptor[] | string[],
-  flags?: TSSymbolFlags,
-): DeclaredTypeParameterDescriptor[];
-function normalizeAndDeclareParameters(
-  parameters: ParameterDescriptor[] | string[],
-  flags?: TSSymbolFlags,
-): DeclaredParameterDescriptor[];
-function normalizeAndDeclareParameters(
-  parameters: string[],
-  flags?: TSSymbolFlags,
-): DeclaredParameterDescriptor[] | DeclaredTypeParameterDescriptor[];
-function normalizeAndDeclareParameters(
-  parameters: ParameterDescriptor[] | TypeParameterDescriptor[] | string[],
-  flags: TSSymbolFlags = TSSymbolFlags.ParameterSymbol,
-): DeclaredParameterDescriptor[] | DeclaredTypeParameterDescriptor[] {
-  const namePolicy = useTSNamePolicy();
-  if (parameters.length === 0) {
-    return [];
-  }
-  if (typeof parameters[0] === "string") {
-    return (parameters as string[]).map((paramName) => {
-      const symbol = createTSSymbol({
-        name: namePolicy.getName(
-          paramName,
-          flags & TSSymbolFlags.TypeSymbol ? "type" : "parameter",
-        ),
-        tsFlags: flags,
-      });
-
-      return { refkeys: symbol.refkeys, symbol };
-    });
-  } else {
-    return (parameters as ParameterDescriptor[]).map((param) => {
-      const symbol = createTSSymbol({
-        name: namePolicy.getName(
-          param.name,
-          flags & TSSymbolFlags.TypeSymbol ? "type" : "parameter",
-        ),
-        refkey: param.refkey,
-        tsFlags: flags,
-        metadata: param.metadata,
-      });
-
-      return {
-        ...param,
-        symbol,
-      };
-    });
-  }
-}
-
-export interface FunctionTypeParametersProps {
-  parameters?: ParameterDescriptor[] | string[];
-  children?: Children;
-}
-
-FunctionDeclaration.TypeParameters = taggedComponent(
-  functionTypeParametersTag,
-  function TypeParameters(props: FunctionTypeParametersProps) {
-    if (props.children) {
-      return props.children;
-    }
-
-    if (!props.parameters) {
-      return undefined;
-    }
-
-    const typeParameters = normalizeAndDeclareParameters(props.parameters);
-
-    onCleanup(() => {
-      for (const param of typeParameters) {
-        param.symbol.binder.deleteSymbol(param.symbol);
-      }
-    });
-
-    return (
-      <>
-        {"<"}
-        <group>
-          <Indent softline>
-            <For each={typeParameters} comma line>
-              {(param) => typeParameter(param)}
-            </For>
-            <ifBreak>,</ifBreak>
-          </Indent>
-        </group>
-        {">"}
-      </>
-    );
-  },
-);
-
-function typeParameter(param: DeclaredTypeParameterDescriptor) {
-  return (
-    <group>
-      {param.symbol.name}
-      <Show when={!!param.extends}>
-        {" "}
-        extends
-        <indent> {param.extends}</indent>
-      </Show>
-    </group>
-  );
-}
-
-export interface FunctionParametersProps {
-  parameters?: ParameterDescriptor[] | string[];
-  children?: Children;
-}
-
-FunctionDeclaration.Parameters = taggedComponent(
-  functionParametersTag,
-  function Parameters(props: FunctionParametersProps) {
-    if (props.children) {
-      return props.children;
-    }
-
-    const parameters = normalizeAndDeclareParameters(props.parameters ?? []);
-    return (
-      <group>
-        <Indent softline trailingBreak>
-          <For each={parameters} comma line>
-            {(param) => parameter(param)}
-          </For>
-          <ifBreak>,</ifBreak>
-        </Indent>
-      </group>
-    );
-  },
-);
-
-function parameter(param: DeclaredParameterDescriptor) {
-  return (
-    <group>
-      {param.symbol.name}
-      <Show when={!!param.optional}>?</Show>
-      <Show when={!!param.type}>
-        <indent>: {param.type}</indent>
-      </Show>
-    </group>
-  );
-}
-
-export interface FunctionBodyProps {
-  children?: Children;
-}
-
-FunctionDeclaration.Body = taggedComponent(
-  functionBodyTag,
-  function Body(props: FunctionBodyProps) {
-    return <Block>{props.children}</Block>;
-  },
-);
+FunctionDeclaration.TypeParameters = FunctionTypeParameters;
+FunctionDeclaration.Parameters = FunctionParameters;
+FunctionDeclaration.Body = FunctionBody;
diff --git a/packages/typescript/src/components/FunctionExpression.tsx b/packages/typescript/src/components/FunctionExpression.tsx
new file mode 100644
index 000000000..35d0eebb3
--- /dev/null
+++ b/packages/typescript/src/components/FunctionExpression.tsx
@@ -0,0 +1,86 @@
+import {
+  childrenArray,
+  findKeyedChild,
+  findUnkeyedChildren,
+  Scope,
+} from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+import { getCallSignatureProps } from "../utils.js";
+import { CallSignature, CallSignatureProps } from "./CallSignature.jsx";
+import { Declaration } from "./Declaration.jsx";
+import {
+  FunctionBody,
+  FunctionParameters,
+  FunctionTypeParameters,
+  getReturnType,
+} from "./FunctionBase.jsx";
+
+export interface FunctionExpressionProps extends CallSignatureProps {
+  async?: boolean;
+  children?: Children;
+}
+
+/**
+ * A TypeScript function expression.
+ *
+ * @example
+ * ```tsx
+ * <FunctionExpression async parameters={[{ name: "a", type: "number" }, { name: "b", type: "number" }]}>
+ *   return a + b;
+ * <FunctionExpression>
+ * ```
+ * renders to
+ * ```ts
+ * async function (a, b) { return a + b; }
+ * ```
+ * @remarks
+ *
+ * Providing parameters and type parameters can be accomplished in one of three
+ * ways:
+ *
+ * 1. As an array of {@link ParameterDescriptor}s or
+ *    {@link TypeParameterDescriptor}s.
+ * 2. As raw content via the `parametersChildren` or `typeParametersChildren`
+ *    props.
+ * 3. As a child of this component via the
+ *    {@link (FunctionExpression:namespace).Parameters} or
+ *    {@link (FunctionExpression:namespace).TypeParameters} components.
+ */
+export function FunctionExpression(props: FunctionExpressionProps) {
+  const children = childrenArray(() => props.children);
+  const typeParametersChildren =
+    findKeyedChild(children, FunctionTypeParameters.tag) ?? undefined;
+  const parametersChildren =
+    findKeyedChild(children, FunctionParameters.tag) ?? undefined;
+  const bodyChildren = findKeyedChild(children, FunctionBody.tag) ?? undefined;
+  const filteredChildren = findUnkeyedChildren(children);
+  const returnType = getReturnType(props.returnType, { async: props.async });
+
+  const sBody = bodyChildren ?? (
+    <FunctionExpression.Body>{filteredChildren}</FunctionExpression.Body>
+  );
+
+  const asyncKwd = props.async ? "async " : "";
+
+  const callSignatureProps = getCallSignatureProps(props, {
+    parametersChildren,
+    typeParametersChildren,
+  });
+
+  return (
+    <>
+      <Declaration {...props} nameKind="function">
+        {asyncKwd}
+        {"function "}
+        <Scope kind="function">
+          <CallSignature {...callSignatureProps} returnType={returnType} />{" "}
+          {sBody}
+        </Scope>
+      </Declaration>
+    </>
+  );
+}
+
+FunctionExpression.TypeParameters = FunctionTypeParameters;
+FunctionExpression.Parameters = FunctionParameters;
+FunctionExpression.Body = FunctionBody;
diff --git a/packages/typescript/src/components/index.ts b/packages/typescript/src/components/index.ts
index 18949cf7c..2ba76d5ea 100644
--- a/packages/typescript/src/components/index.ts
+++ b/packages/typescript/src/components/index.ts
@@ -9,8 +9,15 @@ export * from "./Declaration.js";
 export * from "./EnumDeclaration.js";
 export * from "./EnumMember.js";
 export * from "./ExportStatement.js";
+export {
+  type FunctionBodyProps,
+  type FunctionParametersProps,
+  type FunctionTypeParametersProps,
+} from "./FunctionBase.js";
 export * from "./FunctionCallExpression.js";
 export * from "./FunctionDeclaration.js";
+export * from "./FunctionExpression.js";
+export * from "./ArrowFunction.jsx";
 export * from "./ImportStatement.js";
 export * from "./Interface.js";
 export * from "./JSDoc.jsx";
diff --git a/packages/typescript/src/components/stc/index.ts b/packages/typescript/src/components/stc/index.ts
index 06357f90a..240dd20a8 100644
--- a/packages/typescript/src/components/stc/index.ts
+++ b/packages/typescript/src/components/stc/index.ts
@@ -2,6 +2,7 @@ import { stc } from "@alloy-js/core";
 import * as base from "../index.js";
 
 export const ArrayExpression = stc(base.ArrayExpression);
+export const ArrowFunction = stc(base.ArrowFunction);
 export const BarrelFile = stc(base.BarrelFile);
 export const CallSignature = stc(base.CallSignature);
 export const ClassDeclaration = stc(base.ClassDeclaration);
@@ -15,6 +16,7 @@ export const EnumMember = stc(base.EnumMember);
 export const ExportStatement = stc(base.ExportStatement);
 export const FunctionCallExpression = stc(base.FunctionCallExpression);
 export const FunctionDeclaration = stc(base.FunctionDeclaration);
+export const FunctionExpression = stc(base.FunctionExpression);
 export const ImportStatement = stc(base.ImportStatement);
 export const ImportStatements = stc(base.ImportStatements);
 export const InterfaceDeclaration = stc(base.InterfaceDeclaration);
diff --git a/packages/typescript/test/arrow-function.test.tsx b/packages/typescript/test/arrow-function.test.tsx
new file mode 100644
index 000000000..371a68ea7
--- /dev/null
+++ b/packages/typescript/test/arrow-function.test.tsx
@@ -0,0 +1,203 @@
+import { Props, refkey } from "@alloy-js/core";
+import { d } from "@alloy-js/core/testing";
+import { describe, expect, it } from "vitest";
+import { StatementList } from "../../core/src/components/StatementList.jsx";
+import { ArrowFunction } from "../src/components/ArrowFunction.jsx";
+import { VarDeclaration } from "../src/index.js";
+import { ParameterDescriptor } from "../src/parameter-descriptor.js";
+import { toSourceText } from "./utils.jsx";
+
+it("create basic function", () => {
+  expect(toSourceText(<ArrowFunction />)).toBe(d`
+        () => {}
+    `);
+});
+
+it("can be an async function", () => {
+  expect(toSourceText(<ArrowFunction async />)).toBe(d`
+    async () => {}
+  `);
+});
+
+it("can be an async with returnType", () => {
+  expect(toSourceText(<ArrowFunction async returnType="Foo" />))
+    .toBe(d`
+    async (): Promise<Foo> => {}
+  `);
+});
+
+it("can be an async with returnType element", () => {
+  function Foo(_props?: Props) {
+    return <>Foo</>;
+  }
+  expect(toSourceText(<ArrowFunction async returnType={<Foo />} />))
+    .toBe(d`
+    async (): Promise<Foo> => {}
+  `);
+});
+
+it("supports parameters by element", () => {
+  const decl = (
+    <ArrowFunction>
+      return a + b;
+      <ArrowFunction.Parameters>
+        a, b
+      </ArrowFunction.Parameters>
+    </ArrowFunction>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    (a, b) => {
+      return a + b;
+    }
+  `);
+});
+
+it("supports type parameters by descriptor object", () => {
+  const decl = (
+    <ArrowFunction
+      typeParameters={[
+        { name: "a", extends: "any" },
+        { name: "b", extends: "any" },
+      ]}
+    ></ArrowFunction>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    <a extends any, b extends any>() => {}
+  `);
+});
+
+it("supports type parameters by descriptor array", () => {
+  const decl = (
+    <ArrowFunction
+      typeParameters={["a", "b"]}
+    ></ArrowFunction>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    <a, b>() => {}
+  `);
+});
+
+it("supports type parameters by element", () => {
+  const decl = (
+    <ArrowFunction>
+      <ArrowFunction.TypeParameters>
+        a, b
+      </ArrowFunction.TypeParameters>
+    </ArrowFunction>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    <a, b>() => {}
+  `);
+});
+
+describe("symbols", () => {
+  it("creates a nested scope", () => {
+    const innerRefkey = refkey();
+    const outerRefkey = refkey();
+    const decl = (
+      <StatementList>
+        <ArrowFunction>
+          <StatementList>
+            {innerRefkey}
+            <VarDeclaration name="refme" refkey={innerRefkey}>
+              1
+            </VarDeclaration>
+          </StatementList>
+        </ArrowFunction>
+        <VarDeclaration name="refme" refkey={outerRefkey}>
+          2
+        </VarDeclaration>
+        {outerRefkey}
+      </StatementList>
+    );
+    expect(toSourceText(decl)).toBe(d`
+      () => {
+        refme;
+        const refme = 1;
+      };
+      const refme = 2;
+      refme;
+    `);
+  });
+
+  it("throws an error when trying to access a symbol in a nested scope", () => {
+    const innerRefkey = refkey();
+    const decl = (
+      <>
+        <ArrowFunction>
+          <VarDeclaration name="refme" refkey={innerRefkey}>
+            1
+          </VarDeclaration>
+        </ArrowFunction>
+        ;{innerRefkey}
+      </>
+    );
+    expect(() => toSourceText(decl)).toThrow(/Cannot reference a symbol/);
+  });
+
+  it("creates symbols for parameters", () => {
+    const rk = refkey();
+
+    const decl = (
+      <>
+        <ArrowFunction
+          parameters={[{ name: "sym", type: "any", refkey: rk }]}
+        >
+          <ArrowFunction>{rk}</ArrowFunction>
+        </ArrowFunction>
+      </>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      (sym: any) => {
+        () => {
+          sym
+        }
+      }
+    `);
+  });
+
+  it("creates symbols for parameters and addresses conflicts", () => {
+    const decl = (
+      <>
+        <ArrowFunction
+          parameters={[{ name: "conflict", type: "any" }]}
+        >
+          <VarDeclaration name="conflict">1</VarDeclaration>;
+        </ArrowFunction>
+      </>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      (conflict: any) => {
+        const conflict_2 = 1;
+      }
+    `);
+  });
+
+  it("create optional parameters", () => {
+    const paramDesc: ParameterDescriptor = {
+      name: "foo",
+      refkey: refkey(),
+      type: "any",
+      optional: true,
+    };
+    const decl = (
+      <>
+        <ArrowFunction parameters={[paramDesc]}>
+          console.log(foo);
+        </ArrowFunction>
+      </>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      (foo?: any) => {
+        console.log(foo);
+      }
+    `);
+  });
+});
diff --git a/packages/typescript/test/function-expression.test.tsx b/packages/typescript/test/function-expression.test.tsx
new file mode 100644
index 000000000..508681bbb
--- /dev/null
+++ b/packages/typescript/test/function-expression.test.tsx
@@ -0,0 +1,196 @@
+import { Props, refkey } from "@alloy-js/core";
+import { d } from "@alloy-js/core/testing";
+import { describe, expect, it } from "vitest";
+import { StatementList } from "../../core/src/components/StatementList.jsx";
+import { FunctionExpression } from "../src/components/FunctionExpression.jsx";
+import { VarDeclaration } from "../src/index.js";
+import { ParameterDescriptor } from "../src/parameter-descriptor.js";
+import { toSourceText } from "./utils.jsx";
+
+it("create basic function", () => {
+  expect(toSourceText(<FunctionExpression />)).toBe(d`
+      function () {}
+    `);
+});
+
+it("can be an async function", () => {
+  expect(toSourceText(<FunctionExpression async />)).toBe(d`
+    async function () {}
+  `);
+});
+
+it("can be an async function with returnType", () => {
+  expect(toSourceText(<FunctionExpression async returnType="Foo" />)).toBe(d`
+    async function (): Promise<Foo> {}
+  `);
+});
+
+it("can be an async function with returnType element", () => {
+  function Foo(_props?: Props) {
+    return <>Foo</>;
+  }
+  expect(toSourceText(<FunctionExpression async returnType={<Foo />} />))
+    .toBe(d`
+    async function (): Promise<Foo> {}
+  `);
+});
+
+it("supports parameters by element", () => {
+  const decl = (
+    <FunctionExpression>
+      return a + b;
+      <FunctionExpression.Parameters>a, b</FunctionExpression.Parameters>
+    </FunctionExpression>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    function (a, b) {
+      return a + b;
+    }
+  `);
+});
+
+it("supports type parameters by descriptor object", () => {
+  const decl = (
+    <FunctionExpression
+      typeParameters={[
+        { name: "a", extends: "any" },
+        { name: "b", extends: "any" },
+      ]}
+    ></FunctionExpression>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    function <a extends any, b extends any>() {}
+  `);
+});
+
+it("supports type parameters by descriptor array", () => {
+  const decl = (
+    <FunctionExpression typeParameters={["a", "b"]}></FunctionExpression>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    function <a, b>() {}
+  `);
+});
+
+it("supports type parameters by element", () => {
+  const decl = (
+    <FunctionExpression>
+      <FunctionExpression.TypeParameters>
+        a, b
+      </FunctionExpression.TypeParameters>
+    </FunctionExpression>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    function <a, b>() {}
+  `);
+});
+
+describe("symbols", () => {
+  it("creates a nested scope", () => {
+    const innerRefkey = refkey();
+    const outerRefkey = refkey();
+    const decl = (
+      <StatementList>
+        <FunctionExpression>
+          <StatementList>
+            {innerRefkey}
+            <VarDeclaration name="refme" refkey={innerRefkey}>
+              1
+            </VarDeclaration>
+          </StatementList>
+        </FunctionExpression>
+        <VarDeclaration name="refme" refkey={outerRefkey}>
+          2
+        </VarDeclaration>
+        {outerRefkey}
+      </StatementList>
+    );
+    expect(toSourceText(decl)).toBe(d`
+      function () {
+        refme;
+        const refme = 1;
+      };
+      const refme = 2;
+      refme;
+    `);
+  });
+
+  it("throws an error when trying to access a symbol in a nested function scope", () => {
+    const innerRefkey = refkey();
+    const decl = (
+      <>
+        <FunctionExpression>
+          <VarDeclaration name="refme" refkey={innerRefkey}>
+            1
+          </VarDeclaration>
+        </FunctionExpression>
+        ;{innerRefkey}
+      </>
+    );
+    expect(() => toSourceText(decl)).toThrow(/Cannot reference a symbol/);
+  });
+
+  it("creates symbols for parameters", () => {
+    const rk = refkey();
+
+    const decl = (
+      <>
+        <FunctionExpression
+          parameters={[{ name: "sym", type: "any", refkey: rk }]}
+        >
+          <FunctionExpression>{rk}</FunctionExpression>
+        </FunctionExpression>
+      </>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      function (sym: any) {
+        function () {
+          sym
+        }
+      }
+    `);
+  });
+
+  it("creates symbols for parameters and addresses conflicts", () => {
+    const decl = (
+      <>
+        <FunctionExpression parameters={[{ name: "conflict", type: "any" }]}>
+          <VarDeclaration name="conflict">1</VarDeclaration>;
+        </FunctionExpression>
+      </>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      function (conflict: any) {
+        const conflict_2 = 1;
+      }
+    `);
+  });
+
+  it("create optional parameters", () => {
+    const paramDesc: ParameterDescriptor = {
+      name: "foo",
+      refkey: refkey(),
+      type: "any",
+      optional: true,
+    };
+    const decl = (
+      <>
+        <FunctionExpression parameters={[paramDesc]}>
+          console.log(foo);
+        </FunctionExpression>
+      </>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      function (foo?: any) {
+        console.log(foo);
+      }
+    `);
+  });
+});


Reviews:
user: login: timotheeguerin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T20:18:23Z
user: login: bterlson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:32:17Z
user: login: bterlson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:32:58Z
user: login: bterlson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:36:20Z
user: login: bterlson
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:40:45Z
user: login: timotheeguerin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T20:45:55Z

Review comments:
path: packages/typescript/src/components/FunctionBase.tsx
user: login: timotheeguerin
body: Moved all the common building blocks for function here, not sure what is the current pattern we follow for those, not everything here needs to be rexported (selecting export in index.ts)

Should we move those building block to a sub folder or should the function components be its own folder with all the building block as siblings.
created_at: 2025-04-09T20:18:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,92 @@
+import {
+  childrenArray,
+  findKeyedChild,
+  findUnkeyedChildren,
+  Scope,
+} from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+import { getCallSignatureProps } from "../utils.js";
+import { CallSignature, CallSignatureProps } from "./CallSignature.jsx";
+import { Declaration } from "./Declaration.jsx";
+import {
+  FunctionBody,
+  functionBodyTag,
+  FunctionParameters,
+  functionParametersTag,
+  FunctionTypeParameters,
+  functionTypeParametersTag,
+  getReturnType,
+} from "./FunctionBase.jsx";
+
+export interface FunctionArrowExpressionProps extends CallSignatureProps {
path: packages/typescript/src/components/FunctionArrowExpression.tsx
user: login: bterlson
body: ArrowFunction* I should think (ECMAScript grammar production)
created_at: 2025-04-09T20:32:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+import {
+  childrenArray,
+  findKeyedChild,
+  findUnkeyedChildren,
+  Scope,
+} from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+import { getCallSignatureProps } from "../utils.js";
+import { CallSignature, CallSignatureProps } from "./CallSignature.jsx";
+import { Declaration } from "./Declaration.jsx";
+import {
+  FunctionBody,
+  functionBodyTag,
+  FunctionParameters,
+  functionParametersTag,
+  FunctionTypeParameters,
+  functionTypeParametersTag,
+  getReturnType,
+} from "./FunctionBase.jsx";
+
+export interface FunctionArrowExpressionProps extends CallSignatureProps {
path: packages/typescript/src/components/FunctionArrowExpression.tsx
user: login: bterlson
body: ES grammar doesn't have Expression actually, so fine leaving that out. Looks a lot better :)
created_at: 2025-04-09T20:32:57Z
author_association: CONTRIBUTOR
path: packages/typescript/src/components/FunctionBase.tsx
user: login: bterlson
body: I think this is fine personally, but not opposed to grouping component families together with folders if you want to go that route.
created_at: 2025-04-09T20:36:20Z
author_association: CONTRIBUTOR
path: packages/typescript/src/components/FunctionBase.tsx
user: login: timotheeguerin
body: lets maybe see how other commponents evolve too and I'll refactor if needs be
created_at: 2025-04-09T20:45:55Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-09T20:18:07Z
body: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@alloy-js/core`
 - :white_check_mark: `@alloy-js/typescript`

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

### `@alloy-js/typescript` - _feature_ [✏️](https://github.com/timotheeguerin/alloy/edit/function-expressions/.chronus/changes/function-expressions-2025-3-9-20-17-39.md?pr=/alloy-framework/alloy/pull/91)
> Add new `<FunctionExpression />` and `<ArrowFunctionExpression />` components supporting the various formm of function expressions
### `@alloy-js/core` - _fix_ [✏️](https://github.com/timotheeguerin/alloy/edit/function-expressions/.chronus/changes/function-expressions-2025-3-9-20-41-8.md?pr=/alloy-framework/alloy/pull/91)
> `taggedComponent` return type updated to make tag as a required property
</details>

author_association: CONTRIBUTOR
user: login: pkg-pr-new[bot]
created_at: 2025-04-09T20:18:42Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/29c1628a-56fb-484d-93c7-ba99cc673d39)


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

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

</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@91
```

</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@91
```

</p></details>
      
    

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

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

</p></details>
      
    

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

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/create@91
```

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</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@91
```

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

_commit: <a href="https://github.com/alloy-framework/alloy/runs/40281018301"><code>071c300</code></a>_

author_association: NONE