Repository: alloy-framework/alloy (ts)

Issue:
Add TypeScript FunctionType component
For rendering `(...args) => ...` syntax.
Support interface member methods
Can either add function related props to InterfaceMember, or perhaps introduce a new component `InterfaceMethod` or somesuch.

Pull request:
Add InterfaceMethod and TypeFunction
fix #106
fix #107

Patch:
diff --git a/.chronus/changes/interface-method-2025-3-11-23-32-48.md b/.chronus/changes/interface-method-2025-3-11-23-32-48.md
new file mode 100644
index 0000000..2080def
--- /dev/null
+++ b/.chronus/changes/interface-method-2025-3-11-23-32-48.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: feature
+packages:
+  - "@alloy-js/typescript"
+---
+
+Add `<InterfaceMethod />` and `<FunctionType />` component
diff --git a/.chronus/changes/interface-method-2025-3-14-15-44-45.md b/.chronus/changes/interface-method-2025-3-14-15-44-45.md
new file mode 100644
index 0000000..76b8714
--- /dev/null
+++ b/.chronus/changes/interface-method-2025-3-14-15-44-45.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: fix
+packages:
+  - "@alloy-js/cli"
+---
+
+Recover when transpile error in watch mode
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index 5484578..2aed24a 100644
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -80,7 +80,14 @@ function watchMain() {
   host.afterProgramCreate = async (program) => {
     // eslint-disable-next-line no-console
     console.clear();
-    await buildAllFiles(opts.fileNames, opts.rootDir, opts.outDir);
+    try {
+      await buildAllFiles(opts.fileNames, opts.rootDir, opts.outDir);
+    } catch (e) {
+      // eslint-disable-next-line no-console
+      console.log(pc.red("Error building files"));
+      // eslint-disable-next-line no-console
+      console.log(e);
+    }
     origPostProgramCreate!(program);
   };
   ts.createWatchProgram(host);
diff --git a/packages/typescript/src/components/ArrowFunction.tsx b/packages/typescript/src/components/ArrowFunction.tsx
index 6e861fd..474a6a2 100644
--- a/packages/typescript/src/components/ArrowFunction.tsx
+++ b/packages/typescript/src/components/ArrowFunction.tsx
@@ -11,8 +11,8 @@ import { Declaration } from "./Declaration.jsx";
 import {
   FunctionBody,
   FunctionParameters,
-  FunctionTypeParameters,
   getReturnType,
+  TypeParameters,
 } from "./FunctionBase.jsx";
 
 export interface ArrowFunctionProps extends CallSignatureProps {
@@ -50,7 +50,7 @@ export interface ArrowFunctionProps extends CallSignatureProps {
 export function ArrowFunction(props: ArrowFunctionProps) {
   const children = childrenArray(() => props.children);
   const typeParametersChildren =
-    findKeyedChild(children, FunctionTypeParameters.tag) ?? undefined;
+    findKeyedChild(children, TypeParameters.tag) ?? undefined;
   const parametersChildren =
     findKeyedChild(children, FunctionParameters.tag) ?? undefined;
   const bodyChildren = findKeyedChild(children, FunctionBody.tag) ?? undefined;
@@ -82,6 +82,6 @@ export function ArrowFunction(props: ArrowFunctionProps) {
   );
 }
 
-ArrowFunction.TypeParameters = FunctionTypeParameters;
+ArrowFunction.TypeParameters = TypeParameters;
 ArrowFunction.Parameters = FunctionParameters;
 ArrowFunction.Body = FunctionBody;
diff --git a/packages/typescript/src/components/FunctionBase.tsx b/packages/typescript/src/components/FunctionBase.tsx
index 6bd365a..672c078 100644
--- a/packages/typescript/src/components/FunctionBase.tsx
+++ b/packages/typescript/src/components/FunctionBase.tsx
@@ -9,7 +9,10 @@ import {
 } 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 type {
+  FunctionTypeParameterDescriptor,
+  ParameterDescriptor,
+} from "../parameter-descriptor.js";
 import { TypeParameterDescriptor } from "../parameter-descriptor.js";
 import {
   createTSSymbol,
@@ -18,15 +21,34 @@ import {
 } from "../symbols/index.js";
 
 const functionParametersTag = Symbol();
-const functionTypeParametersTag = Symbol();
+const typeParametersTag = Symbol();
 const functionBodyTag = Symbol();
 
 export interface FunctionBodyProps {
-  children?: Children;
+  readonly children?: Children;
 }
 
+/** Props for function parameters */
 export interface FunctionParametersProps {
-  parameters?: ParameterDescriptor[] | string[];
+  /** Parameters */
+  readonly parameters?: ParameterDescriptor[] | string[];
+  /** Jsx Children */
+  readonly children?: Children;
+}
+
+/** Props for a function type(FunctionType or InterfaceMethod) */
+export interface FunctionTypeParametersProps {
+  /** Parameters */
+  readonly parameters?: ParameterDescriptor[] | string[];
+  /** Jsx Children */
+  readonly children?: Children;
+}
+
+/** Props for type parameters */
+export interface TypeParametersProps {
+  /** Parameters */
+  parameters?: TypeParameterDescriptor[] | string[];
+  /** Jsx Children */
   children?: Children;
 }
 
@@ -37,11 +59,14 @@ export const FunctionBody = taggedComponent(
   },
 );
 
-export interface FunctionTypeParametersProps {
-  parameters?: ParameterDescriptor[] | string[];
-  children?: Children;
-}
-
+/**
+ * Represent function parameters(FunctionDeclaration, FunctionExpression, ArrowFunction, etc.)
+ *
+ * @example
+ * ```ts
+ * a: string, b: string = "abc", c?: string,
+ * ```
+ */
 export const FunctionParameters = taggedComponent(
   functionParametersTag,
   function Parameters(props: FunctionParametersProps) {
@@ -63,6 +88,36 @@ export const FunctionParameters = taggedComponent(
   },
 );
 
+/**
+ * Represent parameters for a function type(FunctionType or InterfaceMethod)
+ * This differ from {@link FunctionParameters} in that it doesn't allow default values.
+ *
+ * @example
+ * ```ts
+ * a: string, b?: string
+ * ```
+ */
+export const FunctionTypeParameters = taggedComponent(
+  functionParametersTag,
+  function Parameters(props: FunctionTypeParametersProps) {
+    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>
@@ -79,15 +134,24 @@ interface DeclaredParameterDescriptor
   extends Omit<ParameterDescriptor, "name"> {
   symbol: TSOutputSymbol;
 }
+interface DeclaredFunctionTypeParameterDescriptor
+  extends Omit<FunctionTypeParameterDescriptor, "name"> {
+  symbol: TSOutputSymbol;
+}
 
 interface DeclaredTypeParameterDescriptor
   extends Omit<TypeParameterDescriptor, "name"> {
   symbol: TSOutputSymbol;
 }
+
 function normalizeAndDeclareParameters(
-  parameters: TypeParameterDescriptor[] | string[],
+  parameters: FunctionTypeParameterDescriptor[] | string[],
   flags?: TSSymbolFlags,
 ): DeclaredTypeParameterDescriptor[];
+function normalizeAndDeclareParameters(
+  parameters: TypeParameterDescriptor[] | string[],
+  flags?: TSSymbolFlags,
+): DeclaredFunctionTypeParameterDescriptor[];
 function normalizeAndDeclareParameters(
   parameters: ParameterDescriptor[] | string[],
   flags?: TSSymbolFlags,
@@ -97,7 +161,11 @@ function normalizeAndDeclareParameters(
   flags?: TSSymbolFlags,
 ): DeclaredParameterDescriptor[] | DeclaredTypeParameterDescriptor[];
 function normalizeAndDeclareParameters(
-  parameters: ParameterDescriptor[] | TypeParameterDescriptor[] | string[],
+  parameters:
+    | ParameterDescriptor[]
+    | FunctionTypeParameterDescriptor[]
+    | TypeParameterDescriptor[]
+    | string[],
   flags: TSSymbolFlags = TSSymbolFlags.ParameterSymbol,
 ): DeclaredParameterDescriptor[] | DeclaredTypeParameterDescriptor[] {
   const namePolicy = useTSNamePolicy();
@@ -136,9 +204,17 @@ function normalizeAndDeclareParameters(
   }
 }
 
-export const FunctionTypeParameters = taggedComponent(
-  functionTypeParametersTag,
-  function TypeParameters(props: FunctionTypeParametersProps) {
+/**
+ * Represent type parameters
+ *
+ * @example
+ * ```ts
+ * <A, B extends string>
+ * ```
+ */
+export const TypeParameters = taggedComponent(
+  typeParametersTag,
+  function TypeParameters(props: TypeParametersProps) {
     if (props.children) {
       return props.children;
     }
diff --git a/packages/typescript/src/components/FunctionDeclaration.tsx b/packages/typescript/src/components/FunctionDeclaration.tsx
index f46fa88..ebe351c 100644
--- a/packages/typescript/src/components/FunctionDeclaration.tsx
+++ b/packages/typescript/src/components/FunctionDeclaration.tsx
@@ -14,8 +14,8 @@ import { BaseDeclarationProps, Declaration } from "./Declaration.js";
 import {
   FunctionBody,
   FunctionParameters,
-  FunctionTypeParameters,
   getReturnType,
+  TypeParameters,
 } from "./FunctionBase.jsx";
 import { JSDoc } from "./JSDoc.jsx";
 import { JSDocParams } from "./JSDocParam.jsx";
@@ -46,7 +46,7 @@ export interface FunctionDeclarationProps
 export function FunctionDeclaration(props: FunctionDeclarationProps) {
   const children = childrenArray(() => props.children);
   const typeParametersChildren =
-    findKeyedChild(children, FunctionTypeParameters.tag) ?? undefined;
+    findKeyedChild(children, TypeParameters.tag) ?? undefined;
   const parametersChildren =
     findKeyedChild(children, FunctionParameters.tag) ?? undefined;
   const bodyChildren = findKeyedChild(children, FunctionBody.tag) ?? undefined;
@@ -86,6 +86,6 @@ export function FunctionDeclaration(props: FunctionDeclarationProps) {
   );
 }
 
-FunctionDeclaration.TypeParameters = FunctionTypeParameters;
+FunctionDeclaration.TypeParameters = TypeParameters;
 FunctionDeclaration.Parameters = FunctionParameters;
 FunctionDeclaration.Body = FunctionBody;
diff --git a/packages/typescript/src/components/FunctionExpression.tsx b/packages/typescript/src/components/FunctionExpression.tsx
index 35d0eeb..ed4c49f 100644
--- a/packages/typescript/src/components/FunctionExpression.tsx
+++ b/packages/typescript/src/components/FunctionExpression.tsx
@@ -11,8 +11,8 @@ import { Declaration } from "./Declaration.jsx";
 import {
   FunctionBody,
   FunctionParameters,
-  FunctionTypeParameters,
   getReturnType,
+  TypeParameters,
 } from "./FunctionBase.jsx";
 
 export interface FunctionExpressionProps extends CallSignatureProps {
@@ -49,7 +49,7 @@ export interface FunctionExpressionProps extends CallSignatureProps {
 export function FunctionExpression(props: FunctionExpressionProps) {
   const children = childrenArray(() => props.children);
   const typeParametersChildren =
-    findKeyedChild(children, FunctionTypeParameters.tag) ?? undefined;
+    findKeyedChild(children, TypeParameters.tag) ?? undefined;
   const parametersChildren =
     findKeyedChild(children, FunctionParameters.tag) ?? undefined;
   const bodyChildren = findKeyedChild(children, FunctionBody.tag) ?? undefined;
@@ -81,6 +81,6 @@ export function FunctionExpression(props: FunctionExpressionProps) {
   );
 }
 
-FunctionExpression.TypeParameters = FunctionTypeParameters;
+FunctionExpression.TypeParameters = TypeParameters;
 FunctionExpression.Parameters = FunctionParameters;
 FunctionExpression.Body = FunctionBody;
diff --git a/packages/typescript/src/components/FunctionType.tsx b/packages/typescript/src/components/FunctionType.tsx
new file mode 100644
index 0000000..f4e3231
--- /dev/null
+++ b/packages/typescript/src/components/FunctionType.tsx
@@ -0,0 +1,88 @@
+import {
+  childrenArray,
+  findKeyedChild,
+  Prose,
+  Scope,
+  Show,
+} from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+import { getCallSignatureProps } from "../utils.js";
+import { CallSignature, CallSignatureProps } from "./CallSignature.jsx";
+import {
+  FunctionParameters,
+  FunctionTypeParameters,
+  getReturnType,
+  TypeParameters,
+} from "./FunctionBase.jsx";
+import { JSDoc } from "./JSDoc.jsx";
+import { JSDocParams } from "./JSDocParam.jsx";
+
+/**
+ * Options for {@link (FunctionType:namespace)} component.
+ */
+export interface FunctionTypeProps extends CallSignatureProps {
+  /** If the method is async */
+  readonly async?: boolean;
+  /** Documentation for this method. */
+  readonly doc?: Children;
+  /** Jsx content */
+  readonly children?: Children;
+}
+
+/**
+ * A TypeScript function declaration.
+ *
+ * @example
+ * ```ts
+ * (foo: string, bar: number) => void
+ * ```
+ * @remarks
+ *
+ * Providing parameters and type parameters can be accomplished in one of three
+ * ways:
+ *
+ * 1. As an array of {@link TypeParameterDescriptor}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 (FunctionType:namespace).Parameters} or
+ *    {@link (FunctionType:namespace).TypeParameters} components.
+ */
+export function FunctionType(props: FunctionTypeProps) {
+  const children = childrenArray(() => props.children);
+  const typeParametersChildren =
+    findKeyedChild(children, TypeParameters.tag) ?? undefined;
+  const parametersChildren =
+    findKeyedChild(children, FunctionParameters.tag) ?? undefined;
+  const returnType = getReturnType(props.returnType ?? "void", {
+    async: props.async,
+  });
+
+  const callSignatureProps = getCallSignatureProps(props, {
+    parametersChildren,
+    typeParametersChildren,
+  });
+
+  return (
+    <>
+      <Show when={Boolean(props.doc)}>
+        <JSDoc>
+          {props.doc && <Prose children={props.doc} />}
+          {Array.isArray(props.parameters) && (
+            <JSDocParams parameters={props.parameters} />
+          )}
+        </JSDoc>
+        <hbr />
+      </Show>
+      <Scope kind="function">
+        <CallSignature {...callSignatureProps} returnType={null} />
+        {" => "}
+        {returnType}
+      </Scope>
+    </>
+  );
+}
+
+FunctionType.TypeParameters = TypeParameters;
+FunctionType.Parameters = FunctionTypeParameters;
diff --git a/packages/typescript/src/components/InterfaceMethod.tsx b/packages/typescript/src/components/InterfaceMethod.tsx
new file mode 100644
index 0000000..d75d5f6
--- /dev/null
+++ b/packages/typescript/src/components/InterfaceMethod.tsx
@@ -0,0 +1,96 @@
+import {
+  childrenArray,
+  findKeyedChild,
+  MemberDeclaration,
+  Prose,
+  Refkey,
+  Scope,
+  Show,
+} from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+import { useTSNamePolicy } from "../name-policy.js";
+import { getCallSignatureProps } from "../utils.js";
+import { CallSignature, CallSignatureProps } from "./CallSignature.jsx";
+import {
+  FunctionParameters,
+  FunctionTypeParameters,
+  getReturnType,
+  TypeParameters,
+} from "./FunctionBase.jsx";
+import { JSDoc } from "./JSDoc.jsx";
+import { JSDocParams } from "./JSDocParam.jsx";
+
+/** Props for {@link (InterfaceMethod:namespace)} component */
+export interface InterfaceMethodProps extends CallSignatureProps {
+  /** Interface member name */
+  readonly name: string;
+  /** If the method is async. It will change the returnType from `T` to `Promise<T>` */
+  readonly async?: boolean;
+  /** Documentation for this method. */
+  readonly doc?: Children;
+  /** Jsx content */
+  readonly children?: Children;
+  /** Ref key for the member declaration */
+  readonly refkey?: Refkey | Refkey[];
+}
+
+/**
+ * An interface method declaration.
+ *
+ * @example
+ * ```ts
+ * (foo: string, bar: number) => void
+ * ```
+ *
+ * @remarks
+ *
+ * Providing parameters and type parameters can be accomplished in one of three
+ * ways:
+ *
+ * 1. As an array of {@link TypeParameterDescriptor}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 (InterfaceMethod:namespace).Parameters} or
+ *    {@link (InterfaceMethod:namespace).TypeParameters} components.
+ */
+export function InterfaceMethod(props: InterfaceMethodProps) {
+  const children = childrenArray(() => props.children);
+  const typeParametersChildren =
+    findKeyedChild(children, TypeParameters.tag) ?? undefined;
+  const parametersChildren =
+    findKeyedChild(children, FunctionParameters.tag) ?? undefined;
+  const returnType = getReturnType(props.returnType ?? "void", {
+    async: props.async,
+  });
+
+  const callSignatureProps = getCallSignatureProps(props, {
+    parametersChildren,
+    typeParametersChildren,
+  });
+  const namer = useTSNamePolicy();
+  const name = namer.getName(props.name!, "interface-member");
+  return (
+    <>
+      <Show when={Boolean(props.doc)}>
+        <JSDoc>
+          {props.doc && <Prose children={props.doc} />}
+          {Array.isArray(props.parameters) && (
+            <JSDocParams parameters={props.parameters} />
+          )}
+        </JSDoc>
+        <hbr />
+      </Show>
+      <MemberDeclaration static {...props} refkey={props.refkey}>
+        {name}
+        <Scope name={props.name} kind="function">
+          <CallSignature {...callSignatureProps} returnType={returnType} />
+        </Scope>
+      </MemberDeclaration>
+    </>
+  );
+}
+
+InterfaceMethod.TypeParameters = TypeParameters;
+InterfaceMethod.Parameters = FunctionTypeParameters;
diff --git a/packages/typescript/src/components/index.ts b/packages/typescript/src/components/index.ts
index d3341f7..0566c21 100644
--- a/packages/typescript/src/components/index.ts
+++ b/packages/typescript/src/components/index.ts
@@ -18,9 +18,11 @@ export {
 export * from "./FunctionCallExpression.js";
 export * from "./FunctionDeclaration.js";
 export * from "./FunctionExpression.js";
+export * from "./FunctionType.jsx";
 export * from "./IfStatement.jsx";
 export * from "./ImportStatement.js";
 export * from "./Interface.js";
+export * from "./InterfaceMethod.jsx";
 export * from "./JSDoc.jsx";
 export * from "./JSDocComment.jsx";
 export * from "./JSDocExample.jsx";
diff --git a/packages/typescript/src/components/stc/index.ts b/packages/typescript/src/components/stc/index.ts
index c44b490..1db3dab 100644
--- a/packages/typescript/src/components/stc/index.ts
+++ b/packages/typescript/src/components/stc/index.ts
@@ -25,6 +25,7 @@ export const ImportStatements = stc(base.ImportStatements);
 export const InterfaceDeclaration = stc(base.InterfaceDeclaration);
 export const InterfaceExpression = stc(base.InterfaceExpression);
 export const InterfaceMember = stc(base.InterfaceMember);
+export const InterfaceMethod = stc(base.InterfaceMethod);
 export const JSDoc = stc(base.JSDoc);
 export const JSDocExample = stc(base.JSDocExample);
 export const JSDocComment = stc(base.JSDocComment);
@@ -39,6 +40,7 @@ export const SourceFile = stc(base.SourceFile);
 export const SwitchStatement = stc(base.SwitchStatement);
 export const CaseClause = stc(base.CaseClause);
 export const TSConfigJson = stc(base.TSConfigJson);
+export const FunctionType = stc(base.FunctionType);
 export const TypeDeclaration = stc(base.TypeDeclaration);
 export const ValueExpression = stc(base.ValueExpression);
 export const VarDeclaration = stc(base.VarDeclaration);
diff --git a/packages/typescript/src/parameter-descriptor.ts b/packages/typescript/src/parameter-descriptor.ts
index 1e4baf1..2856825 100644
--- a/packages/typescript/src/parameter-descriptor.ts
+++ b/packages/typescript/src/parameter-descriptor.ts
@@ -1,45 +1,56 @@
 import { Children, Refkey } from "@alloy-js/core";
 
 /**
- * Information for a TypeScript function parameter.
+ * Common properties for ParameterDescriptor and FunctionTypeParameterDescriptor.
  */
-export interface ParameterDescriptor {
+export interface ParameterDescriptorBase {
   /**
    * The name of the parameter.
    */
-  name: string;
+  readonly name: string;
 
   /**
    * The type of the parameter.
    */
-  type?: Children;
-
-  /**
-   * The default value of the parameter.
-   */
-  default?: Children;
+  readonly type?: Children;
 
   /**
    * The refkey for this parameter.
    */
-  refkey?: Refkey | Refkey[];
+  readonly refkey?: Refkey | Refkey[];
 
   /**
    * Whether the parameter is optional.
    */
-  optional?: boolean;
+  readonly optional?: boolean;
 
   /**
    * Arbitrary metadata for the parameter symbol.
    */
-  metadata?: Record<string, unknown>;
+  readonly metadata?: Record<string, unknown>;
 
   /**
    * Documentation for the parameter.
    */
-  doc?: Children;
+  readonly doc?: Children;
+}
+
+/**
+ * Information for a TypeScript function parameter.
+ */
+export interface ParameterDescriptor extends ParameterDescriptorBase {
+  /**
+   * The default value of the parameter.
+   */
+  readonly default?: Children;
 }
 
+/**
+ * Information for a TypeScript type function parameter.
+ */
+export interface FunctionTypeParameterDescriptor
+  extends ParameterDescriptorBase {}
+
 /**
  * Information for a TypeScript generic type parameter.
  */
@@ -47,25 +58,25 @@ export interface TypeParameterDescriptor {
   /**
    * The name of the type parameter.
    */
-  name: string;
+  readonly name: string;
 
   /**
    * The extends constraint for the type parameter.
    */
-  extends?: Children;
+  readonly extends?: Children;
 
   /**
    * The default type of the type parameter.
    */
-  default?: Children;
+  readonly default?: Children;
 
   /**
    * A refkey or array of refkeys for this type parameter.
    */
-  refkey?: Refkey | Refkey[];
+  readonly refkey?: Refkey | Refkey[];
 
   /**
    * Arbitrary metadata for the type parameter symbol.
    */
-  metadata?: Record<string, unknown>;
+  readonly metadata?: Record<string, unknown>;
 }


Test patch:
diff --git a/packages/typescript/test/function-type.test.tsx b/packages/typescript/test/function-type.test.tsx
new file mode 100644
index 0000000..839d956
--- /dev/null
+++ b/packages/typescript/test/function-type.test.tsx
@@ -0,0 +1,118 @@
+import { Props, refkey } from "@alloy-js/core";
+import "@alloy-js/core/testing";
+import { d } from "@alloy-js/core/testing";
+import { describe, expect, it } from "vitest";
+import { FunctionType } from "../src/components/FunctionType.jsx";
+import * as ts from "../src/components/index.js";
+import { ParameterDescriptor } from "../src/components/index.js";
+import { toSourceText } from "./utils.jsx";
+
+it("render basic", () => {
+  expect(toSourceText(<FunctionType />)).toBe(d`
+    () => void
+    `);
+});
+
+it("render in interface", () => {
+  expect(
+    toSourceText(
+      <ts.InterfaceDeclaration name="Foo">
+        <ts.InterfaceMember name="foo" type={<FunctionType />} />
+      </ts.InterfaceDeclaration>,
+    ),
+  ).toBe(d`
+      interface Foo {
+        foo: () => void
+      }
+    `);
+});
+
+describe("marking it as async", () => {
+  it("no return type change to Promise<void>", () => {
+    expect(toSourceText(<FunctionType async />)).toBe(d`
+      () => Promise<void>
+    `);
+  });
+
+  it("explicit returnType change to Promise<T>", () => {
+    expect(toSourceText(<FunctionType async returnType="Foo" />)).toBe(d`
+      () => Promise<Foo>
+    `);
+  });
+
+  it("component returnType change to Promise<T>", () => {
+    function Foo(_props?: Props) {
+      return <>Foo</>;
+    }
+    expect(toSourceText(<FunctionType async returnType={<Foo />} />)).toBe(d`
+    () => Promise<Foo>
+  `);
+  });
+});
+
+it("supports parameters by element", () => {
+  const decl = (
+    <FunctionType>
+      <FunctionType.Parameters>a, b</FunctionType.Parameters>
+    </FunctionType>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    (a, b) => void
+  `);
+});
+
+it("supports type parameters by descriptor object", () => {
+  const decl = (
+    <FunctionType
+      typeParameters={[
+        { name: "a", extends: "any" },
+        { name: "b", extends: "any" },
+      ]}
+    ></FunctionType>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    <a extends any, b extends any>() => void
+  `);
+});
+
+it("supports type parameters by descriptor array", () => {
+  const decl = <FunctionType typeParameters={["a", "b"]}></FunctionType>;
+
+  expect(toSourceText(decl)).toBe(d`
+    <a, b>() => void
+  `);
+});
+
+it("supports type parameters by element", () => {
+  const decl = (
+    <FunctionType>
+      <FunctionType.TypeParameters>a, b</FunctionType.TypeParameters>
+    </FunctionType>
+  );
+
+  expect(toSourceText(decl)).toBe(d`
+    <a, b>() => void
+  `);
+});
+
+describe("symbols", () => {
+  it("create optional parameters", () => {
+    const paramDesc: ParameterDescriptor = {
+      name: "foo",
+      refkey: refkey(),
+      type: "any",
+      optional: true,
+    };
+    const decl = (
+      <>
+        <FunctionType parameters={[paramDesc]}></FunctionType>
+      </>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      (foo?: any) => void
+    `);
+  });
+});
diff --git a/packages/typescript/test/interface.test.tsx b/packages/typescript/test/interface.test.tsx
index 186a4fc..6b579c6 100644
--- a/packages/typescript/test/interface.test.tsx
+++ b/packages/typescript/test/interface.test.tsx
@@ -1,8 +1,14 @@
-import { NamePolicyContext, refkey, StatementList } from "@alloy-js/core";
+import {
+  NamePolicyContext,
+  Props,
+  refkey,
+  StatementList,
+} from "@alloy-js/core";
 import "@alloy-js/core/testing";
 import { d } from "@alloy-js/core/testing";
-import { expect, it } from "vitest";
+import { describe, expect, it } from "vitest";
 import * as ts from "../src/components/index.js";
+import { ParameterDescriptor } from "../src/components/index.js";
 import { Reference } from "../src/components/Reference.js";
 import { createTSNamePolicy } from "../src/name-policy.js";
 import { toSourceText } from "./utils.js";
@@ -146,3 +152,171 @@ it("handles invalid identifier names", () => {
     }
   `);
 });
+
+describe("method members", () => {
+  it("render basic", () => {
+    expect(
+      toSourceText(
+        <ts.InterfaceDeclaration name="Foo">
+          <ts.InterfaceMethod name="foo" />
+        </ts.InterfaceDeclaration>,
+      ),
+    ).toBe(d`
+      interface Foo {
+        foo(): void
+      }
+    `);
+  });
+
+  it("render in interface", () => {
+    expect(
+      toSourceText(
+        <ts.InterfaceDeclaration name="Foo">
+          <ts.InterfaceMethod name="foo" />
+        </ts.InterfaceDeclaration>,
+      ),
+    ).toBe(d`
+      interface Foo {
+        foo(): void
+      }
+    `);
+  });
+
+  it("can be an async function", () => {
+    expect(
+      toSourceText(
+        <ts.InterfaceDeclaration name="Foo">
+          <ts.InterfaceMethod async name="foo" />
+        </ts.InterfaceDeclaration>,
+      ),
+    ).toBe(d`
+    interface Foo {
+      foo(): Promise<void>
+    }
+  `);
+  });
+
+  it("can be an async function with returnType", () => {
+    expect(
+      toSourceText(
+        <ts.InterfaceDeclaration name="Foo">
+          <ts.InterfaceMethod async name="foo" returnType="Foo" />
+        </ts.InterfaceDeclaration>,
+      ),
+    ).toBe(d`
+    interface Foo {
+      foo(): Promise<Foo>
+    }
+  `);
+  });
+
+  it("can be an async function with returnType element", () => {
+    function Foo(_props?: Props) {
+      return <>Foo</>;
+    }
+    expect(
+      toSourceText(
+        <ts.InterfaceDeclaration name="Foo">
+          <ts.InterfaceMethod async name="foo" returnType={<Foo />} />
+        </ts.InterfaceDeclaration>,
+      ),
+    ).toBe(d`
+    interface Foo {
+      foo(): Promise<Foo>
+    }
+  `);
+  });
+
+  it("supports parameters by element", () => {
+    const decl = (
+      <ts.InterfaceDeclaration name="Foo">
+        <ts.InterfaceMethod name="foo">
+          <ts.InterfaceMethod.Parameters>a, b</ts.InterfaceMethod.Parameters>
+        </ts.InterfaceMethod>
+      </ts.InterfaceDeclaration>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+    interface Foo {
+      foo(a, b): void
+    }
+  `);
+  });
+
+  it("supports type parameters by descriptor object", () => {
+    const decl = (
+      <ts.InterfaceDeclaration name="Foo">
+        <ts.InterfaceMethod
+          name="foo"
+          typeParameters={[
+            { name: "a", extends: "any" },
+            { name: "b", extends: "any" },
+          ]}
+        />
+      </ts.InterfaceDeclaration>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+    interface Foo {
+      foo<a extends any, b extends any>(): void
+    }
+  `);
+  });
+
+  it("supports type parameters by descriptor array", () => {
+    const decl = (
+      <ts.InterfaceDeclaration name="Foo">
+        <ts.InterfaceMethod
+          name="foo"
+          typeParameters={["a", "b"]}
+        ></ts.InterfaceMethod>
+      </ts.InterfaceDeclaration>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+      interface Foo {
+        foo<a, b>(): void
+      }
+  `);
+  });
+
+  it("supports type parameters by element", () => {
+    const decl = (
+      <ts.InterfaceDeclaration name="Foo">
+        <ts.InterfaceMethod name="foo">
+          <ts.InterfaceMethod.TypeParameters>
+            a, b
+          </ts.InterfaceMethod.TypeParameters>
+        </ts.InterfaceMethod>
+      </ts.InterfaceDeclaration>
+    );
+
+    expect(toSourceText(decl)).toBe(d`
+    interface Foo {
+      foo<a, b>(): void
+    }
+  `);
+  });
+
+  describe("symbols", () => {
+    it("create optional parameters", () => {
+      const paramDesc: ParameterDescriptor = {
+        name: "foo",
+        refkey: refkey(),
+        type: "any",
+        optional: true,
+      };
+      const decl = (
+        <ts.InterfaceDeclaration name="Foo">
+          <ts.InterfaceMethod name="foo" parameters={[paramDesc]} />
+        </ts.InterfaceDeclaration>
+      );
+
+      expect(toSourceText(decl)).toBe(d`
+      interface Foo {
+        foo(foo?: any): void
+      }
+    `);
+    });
+  });
+});