Repository: alloy-framework/alloy
Pull request #236 (2025-07-07 23:21:51)

typescript: implement type parameters for interfaces, type parameter defaults
Like parameter defaults, type parameter defaults were unimplemented, so this PR implements them.

This PR implements support for type parameters in interface declarations.

github-actions[bot]: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@alloy-js/typescript`

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

### `@alloy-js/typescript` - _feature_ [✏️](https://github.com/witemple-msft/alloy/edit/witemple-msft/interface-type-params/.chronus/changes/witemple-msft-interface-type-params-2025-6-7-19-18-6.md?pr=/alloy-framework/alloy/pull/236)
> Implemented type parameters for InterfaceDeclaration components.,> ,> Implemented type parameter defaults.
</details>

pkg-pr-new[bot]: 
[Open in StackBlitz](https://pkg.pr.new/template/2da936d4-eec7-4dce-aa7a-3c8ac901861d)


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

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

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

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

</p></details>
      
    

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

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/cli@236
```

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/markdown@236
```

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

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

</p></details>
      
    

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

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

</p></details>
      
    

_commit: <a href="https://github.com/alloy-framework/alloy/runs/45515488064"><code>600431c</code></a>_


Patch:
diff --git a/.chronus/changes/json-values-compose-2025-6-8-20-10-8.md b/.chronus/changes/json-values-compose-2025-6-8-20-10-8.md
new file mode 100644
index 0000000..eee525b
--- /dev/null
+++ b/.chronus/changes/json-values-compose-2025-6-8-20-10-8.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: feature
+packages:
+  - "@alloy-js/json"
+---
+
+Support nesting components in `<JsonValue jsValue=`
diff --git a/.chronus/changes/witemple-msft-interface-type-params-2025-6-7-19-18-6.md b/.chronus/changes/witemple-msft-interface-type-params-2025-6-7-19-18-6.md
new file mode 100644
index 0000000..51c8dcc
--- /dev/null
+++ b/.chronus/changes/witemple-msft-interface-type-params-2025-6-7-19-18-6.md
@@ -0,0 +1,9 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/typescript"
+---
+
+Implemented type parameters for InterfaceDeclaration components.
+
+Implemented type parameter defaults.
diff --git a/packages/json/package.json b/packages/json/package.json
index 7c9c3df..d3e539f 100644
--- a/packages/json/package.json
+++ b/packages/json/package.json
@@ -12,6 +12,9 @@
       "import": "./dist/src/components/stc/index.js"
     }
   },
+  "imports": {
+    "#test/*": "./test/*"
+  },
   "scripts": {
     "generate-docs": "api-extractor run",
     "build": "alloy build && pnpm run generate-docs",
diff --git a/packages/json/src/components/index.ts b/packages/json/src/components/index.ts
index 00cc49e..726a43a 100644
--- a/packages/json/src/components/index.ts
+++ b/packages/json/src/components/index.ts
@@ -1,5 +1,5 @@
-export * from "./JsonArray.jsx";
-export * from "./JsonObject.jsx";
-export * from "./JsonValue.jsx";
-export * from "./Reference.jsx";
-export * from "./SourceFile.jsx";
+export * from "./json-array.jsx";
+export * from "./json-object.jsx";
+export * from "./json-value.jsx";
+export * from "./reference.jsx";
+export * from "./source-file.jsx";
diff --git a/packages/json/src/components/JsonArray.tsx b/packages/json/src/components/json-array.tsx
similarity index 98%
rename from packages/json/src/components/JsonArray.tsx
rename to packages/json/src/components/json-array.tsx
index 974a73e..0753bd6 100644
--- a/packages/json/src/components/JsonArray.tsx
+++ b/packages/json/src/components/json-array.tsx
@@ -12,7 +12,7 @@ import {
   useMemberScope,
 } from "@alloy-js/core";
 import { JsonOutputSymbol } from "../symbols/json-symbol.js";
-import { JsonValue } from "./JsonValue.jsx";
+import { JsonValue } from "./json-value.jsx";
 
 export interface JsonArrayPropsBase {
   /**
diff --git a/packages/json/src/components/JsonObject.tsx b/packages/json/src/components/json-object.tsx
similarity index 98%
rename from packages/json/src/components/JsonObject.tsx
rename to packages/json/src/components/json-object.tsx
index 2eef6c6..135c589 100644
--- a/packages/json/src/components/JsonObject.tsx
+++ b/packages/json/src/components/json-object.tsx
@@ -12,7 +12,7 @@ import {
   useMemberDeclaration,
 } from "@alloy-js/core";
 import { JsonOutputSymbol } from "../symbols/json-symbol.js";
-import { JsonValue } from "./JsonValue.jsx";
+import { JsonValue } from "./json-value.jsx";
 
 export interface JsonObjectPropsBase {
   /**
diff --git a/packages/json/src/components/JsonValue.tsx b/packages/json/src/components/json-value.tsx
similarity index 77%
rename from packages/json/src/components/JsonValue.tsx
rename to packages/json/src/components/json-value.tsx
index 6fc1caf..15b7378 100644
--- a/packages/json/src/components/JsonValue.tsx
+++ b/packages/json/src/components/json-value.tsx
@@ -1,6 +1,6 @@
-import { Refkey, useMemberDeclaration } from "@alloy-js/core";
-import { JsonArray } from "./JsonArray.jsx";
-import { JsonObject } from "./JsonObject.jsx";
+import { Children, Refkey, useMemberDeclaration } from "@alloy-js/core";
+import { JsonArray } from "./json-array.jsx";
+import { JsonObject } from "./json-object.jsx";
 
 export interface JsonValueProps {
   /**
@@ -22,7 +22,7 @@ export interface JsonValueProps {
  * value will be serialized to a string, number, boolean, or null as
  * appropriate.
  */
-export function JsonValue(props: JsonValueProps) {
+export function JsonValue(props: JsonValueProps): Children {
   const refkeys = [props.refkey ?? []].flat();
   if (props.jsValue === null) {
     setMemberRefkey(refkeys);
@@ -44,7 +44,12 @@ export function JsonValue(props: JsonValueProps) {
   }
 
   if (typeof props.jsValue === "object") {
-    return <JsonObject jsValue={props.jsValue as any} />;
+    return <JsonObject jsValue={props.jsValue as Record<string, unknown>} />;
+  }
+
+  if (typeof props.jsValue === "function") {
+    // functions are inserted as-is.
+    return props.jsValue as () => Children;
   }
 
   throw new Error("Cannot emit js value with type of " + typeof props.jsValue);
diff --git a/packages/json/src/components/Reference.tsx b/packages/json/src/components/reference.tsx
similarity index 100%
rename from packages/json/src/components/Reference.tsx
rename to packages/json/src/components/reference.tsx
diff --git a/packages/json/src/components/SourceFile.tsx b/packages/json/src/components/source-file.tsx
similarity index 96%
rename from packages/json/src/components/SourceFile.tsx
rename to packages/json/src/components/source-file.tsx
index 85a0c84..aebaf79 100644
--- a/packages/json/src/components/SourceFile.tsx
+++ b/packages/json/src/components/source-file.tsx
@@ -6,7 +6,7 @@ import {
 } from "@alloy-js/core";
 import { JsonFileContext } from "../context/JsonFileContext.js";
 import { JsonOutputSymbol } from "../symbols/json-symbol.js";
-import { Reference } from "./Reference.jsx";
+import { Reference } from "./reference.jsx";
 
 export interface SourceFileProps {
   /** The path for this source file relative to the parent directory */
diff --git a/packages/typescript/src/components/Declaration.tsx b/packages/typescript/src/components/Declaration.tsx
index 7882967..53e4378 100644
--- a/packages/typescript/src/components/Declaration.tsx
+++ b/packages/typescript/src/components/Declaration.tsx
@@ -15,8 +15,6 @@ import { TypeDeclaration } from "./TypeDeclaration.jsx";
 import { PrivateScopeContext } from "../context/private-scope.js";
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 import { EnumDeclaration } from "./EnumDeclaration.jsx";
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { InterfaceDeclaration } from "./Interface.jsx";
 
 export interface BaseDeclarationProps {
   /**
@@ -84,7 +82,7 @@ export interface DeclarationProps extends Omit<BaseDeclarationProps, "name"> {
 /**
  * Create a TypeScript declaration. Generally, this component shouldn't be used
  * directly, and instead prefer components for specific declarations, e.g.
- * {@link EnumDeclaration}, {@link InterfaceDeclaration},
+ * {@link EnumDeclaration}, {@link (InterfaceDeclaration:function)},
  * {@link TypeDeclaration}, etc.
  *
  * @remarks
diff --git a/packages/typescript/src/components/FunctionBase.tsx b/packages/typescript/src/components/FunctionBase.tsx
index b959715..9707a43 100644
--- a/packages/typescript/src/components/FunctionBase.tsx
+++ b/packages/typescript/src/components/FunctionBase.tsx
@@ -288,6 +288,10 @@ function typeParameter(param: DeclaredTypeParameterDescriptor) {
           <TypeRefContext>{param.extends}</TypeRefContext>
         </indent>
       </Show>
+      <Show when={!!param.default}>
+        {" = "}
+        <TypeRefContext>{param.default}</TypeRefContext>
+      </Show>
     </group>
   );
 }
diff --git a/packages/typescript/src/components/Interface.tsx b/packages/typescript/src/components/Interface.tsx
index 164daf7..4a1de61 100644
--- a/packages/typescript/src/components/Interface.tsx
+++ b/packages/typescript/src/components/Interface.tsx
@@ -1,6 +1,9 @@
 import {
   Block,
   Children,
+  childrenArray,
+  findKeyedChild,
+  findUnkeyedChildren,
   MemberScope,
   Name,
   OutputSymbolFlags,
@@ -14,28 +17,42 @@ import { useTSNamePolicy } from "../name-policy.js";
 import { BaseDeclarationProps, Declaration } from "./Declaration.js";
 import { JSDoc } from "./JSDoc.jsx";
 
+import { TypeParameterDescriptor } from "../parameter-descriptor.js";
+import { TypeParameters } from "./FunctionBase.jsx";
 import { MemberDeclaration } from "./MemberDeclaration.jsx";
 import { PropertyName } from "./PropertyName.jsx";
 import { ensureTypeRefContext } from "./TypeRefContext.jsx";
 
 export interface InterfaceDeclarationProps extends BaseDeclarationProps {
   extends?: Children;
+
+  /**
+   * The generic type parameters of the interface.
+   */
+  typeParameters?: TypeParameterDescriptor[] | string[];
 }
 
-/**
- * Create a TypeScript interface declaration.
- *
- * @remarks
- *
- * This component will declare a symbol for this interface. The `export` and
- * `default` boolean props determine whether and how this symbol is exported
- * from the package.
- */
-export const InterfaceDeclaration = ensureTypeRefContext(
+const _InterfaceDeclaration = ensureTypeRefContext(
   (props: InterfaceDeclarationProps) => {
+    const children = childrenArray(() => props.children);
+
+    const typeParametersChildren =
+      findKeyedChild(children, TypeParameters.tag) ?? undefined;
+
+    const sTypeParameters =
+      typeParametersChildren ?
+        <>
+          {"<"}
+          {typeParametersChildren}
+          {">"}
+        </>
+      : <TypeParameters parameters={props.typeParameters} />;
+
     const extendsPart = props.extends ? <> extends {props.extends}</> : "";
     const flags = OutputSymbolFlags.StaticMemberContainer;
 
+    const filteredChildren = findUnkeyedChildren(children);
+
     return (
       <>
         <Show when={Boolean(props.doc)}>
@@ -44,14 +61,30 @@ export const InterfaceDeclaration = ensureTypeRefContext(
         </Show>
         <Declaration {...props} nameKind="interface" flags={flags} kind="type">
           interface <Name />
+          {sTypeParameters}
           {extendsPart}{" "}
-          <InterfaceExpression>{props.children}</InterfaceExpression>
+          <InterfaceExpression>{filteredChildren}</InterfaceExpression>
         </Declaration>
       </>
     );
   },
 );
 
+/**
+ * Create a TypeScript interface declaration.
+ *
+ * @remarks
+ *
+ * This component will declare a symbol for this interface. The `export` and
+ * `default` boolean props determine whether and how this symbol is exported
+ * from the package.
+ */
+export function InterfaceDeclaration(props: InterfaceDeclarationProps) {
+  return _InterfaceDeclaration(props);
+}
+
+InterfaceDeclaration.TypeParameters = TypeParameters;
+
 export interface InterfaceExpressionProps {
   children?: Children;
 }


Test patch:
diff --git a/packages/json/test/array.test.tsx b/packages/json/src/components/array.test.tsx
similarity index 96%
rename from packages/json/test/array.test.tsx
rename to packages/json/src/components/array.test.tsx
index 3efcff7..39be7eb 100644
--- a/packages/json/test/array.test.tsx
+++ b/packages/json/src/components/array.test.tsx
@@ -2,7 +2,7 @@ import { reactive, renderTree } from "@alloy-js/core";
 import "@alloy-js/core/testing";
 import { d, printTree } from "@alloy-js/core/testing";
 import { expect, it } from "vitest";
-import { jsonTest } from "./utils.jsx";
+import { jsonTest } from "../../test/utils.jsx";
 
 it("renders empty arrays", () => {
   const template = jsonTest([]);
diff --git a/packages/json/src/components/json-value.test.tsx b/packages/json/src/components/json-value.test.tsx
new file mode 100644
index 0000000..abfdf1e
--- /dev/null
+++ b/packages/json/src/components/json-value.test.tsx
@@ -0,0 +1,44 @@
+import { Output } from "@alloy-js/core";
+import { expect, it } from "vitest";
+import { JsonValue } from "./json-value.jsx";
+import { SourceFile } from "./source-file.jsx";
+
+const TestingJsonObject = () => {
+  return (
+    <JsonValue
+      jsValue={{
+        id: "test-id",
+        name: "Test Name",
+        nested: () => <Bar />,
+      }}
+    />
+  );
+};
+
+const Bar = () => {
+  return (
+    <JsonValue
+      jsValue={{
+        bar: "Foo",
+      }}
+    />
+  );
+};
+
+it("compose json data with components", () => {
+  expect(
+    <Output>
+      <SourceFile path="test.json">
+        <TestingJsonObject />
+      </SourceFile>
+    </Output>,
+  ).toRenderTo(`
+    {
+      "id": "test-id",
+      "name": "Test Name",
+      "nested": {
+        "bar": "Foo"
+      }
+    }
+  `);
+});
diff --git a/packages/json/test/object.test.tsx b/packages/json/src/components/object.test.tsx
similarity index 88%
rename from packages/json/test/object.test.tsx
rename to packages/json/src/components/object.test.tsx
index 02bc72d..1c9f15e 100644
--- a/packages/json/test/object.test.tsx
+++ b/packages/json/src/components/object.test.tsx
@@ -2,12 +2,9 @@ import { List, Output, reactive, renderTree } from "@alloy-js/core";
 import "@alloy-js/core/testing";
 import { d, printTree } from "@alloy-js/core/testing";
 import { expect, it } from "vitest";
-import {
-  JsonObject,
-  JsonObjectProperty,
-} from "../src/components/JsonObject.jsx";
-import { SourceFile } from "../src/components/SourceFile.jsx";
-import { jsonTest } from "./utils.jsx";
+import { jsonTest } from "../../test/utils.jsx";
+import { JsonObject, JsonObjectProperty } from "./json-object.jsx";
+import { SourceFile } from "./source-file.jsx";
 
 it("renders objects", () => {
   const template = jsonTest({ a: 1, b: "hello" });
diff --git a/packages/json/src/components/primitives.test.tsx b/packages/json/src/components/primitives.test.tsx
new file mode 100644
index 0000000..85d2478
--- /dev/null
+++ b/packages/json/src/components/primitives.test.tsx
@@ -0,0 +1,15 @@
+import { jsonTest } from "#test/utils.jsx";
+import "@alloy-js/core/testing";
+import { expect, it } from "vitest";
+
+it.each([
+  ["boolean (true)", true, "true"],
+  ["boolean (false)", false, "false"],
+  ["integer", 10, "10"],
+  ["negative integer", -10, "-10"],
+  ["float", 1.314, "1.314"],
+  ["string", "Hello, world!", `"Hello, world!"`],
+  ["null", null, "null"],
+])("%s", (_, data, expected) => {
+  expect(jsonTest(data)).toRenderTo(expected);
+});
diff --git a/packages/json/test/reference.test.tsx b/packages/json/src/components/reference.test.tsx
similarity index 94%
rename from packages/json/test/reference.test.tsx
rename to packages/json/src/components/reference.test.tsx
index 578e872..87a56d7 100644
--- a/packages/json/test/reference.test.tsx
+++ b/packages/json/src/components/reference.test.tsx
@@ -1,13 +1,10 @@
 import { Output, refkey, SourceDirectory } from "@alloy-js/core";
 import "@alloy-js/core/testing";
 import { expect, it } from "vitest";
-import { JsonArray, JsonArrayElement } from "../src/components/JsonArray.jsx";
-import {
-  JsonObject,
-  JsonObjectProperty,
-} from "../src/components/JsonObject.jsx";
-import { JsonValue } from "../src/components/JsonValue.jsx";
-import { SourceFile } from "../src/components/SourceFile.jsx";
+import { JsonArray, JsonArrayElement } from "./json-array.jsx";
+import { JsonObject, JsonObjectProperty } from "./json-object.jsx";
+import { JsonValue } from "./json-value.jsx";
+import { SourceFile } from "./source-file.jsx";
 
 it("references objects within the same file", () => {
   const objectValue = refkey();
diff --git a/packages/json/test/primitives.test.tsx b/packages/json/test/primitives.test.tsx
deleted file mode 100644
index 95647bb..0000000
--- a/packages/json/test/primitives.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import "@alloy-js/core/testing";
-import { expect, it } from "vitest";
-import { jsonTest } from "./utils.jsx";
-
-it("renders primitives", () => {
-  expect(jsonTest(true)).toRenderTo("true");
-  expect(jsonTest(false)).toRenderTo("false");
-
-  expect(jsonTest(10)).toRenderTo("10");
-  expect(jsonTest(-10)).toRenderTo("-10");
-  expect(jsonTest(1.314)).toRenderTo("1.314");
-
-  expect(jsonTest("hello")).toRenderTo('"hello"');
-
-  expect(jsonTest(null)).toRenderTo("null");
-});
diff --git a/packages/json/test/utils.tsx b/packages/json/test/utils.tsx
index aca10e7..a1c4a34 100644
--- a/packages/json/test/utils.tsx
+++ b/packages/json/test/utils.tsx
@@ -1,6 +1,6 @@
 import { Output } from "@alloy-js/core";
-import { JsonValue } from "../src/components/JsonValue.jsx";
-import { SourceFile } from "../src/components/SourceFile.jsx";
+import { JsonValue } from "../src/components/json-value.jsx";
+import { SourceFile } from "../src/components/source-file.jsx";
 
 export function jsonTest(jsValue: unknown) {
   return (
diff --git a/packages/json/test/vitest.d.ts b/packages/json/test/vitest.d.ts
deleted file mode 100644
index c4c777d..0000000
--- a/packages/json/test/vitest.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import "vitest";
-
-interface ToRenderToRenderOptions {
-  printWidth?: number;
-  tabWidth?: number;
-  useTabs?: boolean;
-}
-
-interface CustomMatchers<R = unknown> {
-  toRenderTo: (str: string, options?: ToRenderToRenderOptions) => R;
-}
-
-declare module "vitest" {
-  interface Assertion<T = any> extends CustomMatchers<T> {}
-  interface AsymmetricMatchersContaining extends CustomMatchers {}
-}
diff --git a/packages/json/test/vitest.setup.ts b/packages/json/test/vitest.setup.ts
new file mode 100644
index 0000000..96cd441
--- /dev/null
+++ b/packages/json/test/vitest.setup.ts
@@ -0,0 +1,1 @@
+import "@alloy-js/core/testing";
diff --git a/packages/json/vitest.config.ts b/packages/json/vitest.config.ts
index ba661c0..a984519 100644
--- a/packages/json/vitest.config.ts
+++ b/packages/json/vitest.config.ts
@@ -6,5 +6,8 @@ export default defineConfig({
     jsx: "preserve",
     sourcemap: "both",
   },
+  test: {
+    setupFiles: ["./test/vitest.setup.ts"],
+  },
   plugins: [alloyPlugin()],
 });
diff --git a/packages/typescript/test/interface.test.tsx b/packages/typescript/test/interface.test.tsx
index 22ec7c6..e499524 100644
--- a/packages/typescript/test/interface.test.tsx
+++ b/packages/typescript/test/interface.test.tsx
@@ -229,6 +229,76 @@ it("handles invalid identifier names", () => {
   `);
 });
 
+it("accepts type parameters by descriptors", () => {
+  const typeParams: ts.TypeParameterDescriptor[] = [
+    { name: "T", refkey: refkey() },
+    { name: "U", extends: "number", refkey: refkey() },
+    { name: "V", default: "object", refkey: refkey() },
+    { name: "W", extends: "string", default: '"test"', refkey: refkey() },
+  ];
+
+  const res = toSourceText(
+    <ts.InterfaceDeclaration name="Foo" typeParameters={typeParams}>
+      <ts.InterfaceMember name="member" type={typeParams[0].refkey} />;
+      <hbr />
+      <ts.InterfaceMember name="member2" type={typeParams[1].refkey} />;
+      <hbr />
+      <ts.InterfaceMember name="member3" type={typeParams[2].refkey} />;
+      <hbr />
+      <ts.InterfaceMember name="member4" type={typeParams[3].refkey} />;
+    </ts.InterfaceDeclaration>,
+  );
+
+  expect(res).toEqual(d`
+    interface Foo<T, U extends number, V = object, W extends string = "test"> {
+      member: T;
+      member2: U;
+      member3: V;
+      member4: W;
+    }
+  `);
+});
+
+it("accepts type parameters with extends", () => {
+  const res = toSourceText(
+    <ts.InterfaceDeclaration name="Foo" typeParameters={["T"]} extends="Bar">
+      <ts.InterfaceMember name="member" type="T" />;
+    </ts.InterfaceDeclaration>,
+  );
+
+  expect(res).toEqual(d`
+    interface Foo<T> extends Bar {
+      member: T;
+    }
+  `);
+});
+
+it("accepts type parameters children", () => {
+  const res = toSourceText(
+    <ts.InterfaceDeclaration name="Foo">
+      <ts.InterfaceDeclaration.TypeParameters>
+        T, U extends number, V = object, W extends string = "test"
+      </ts.InterfaceDeclaration.TypeParameters>
+      <ts.InterfaceMember name="member" type={"T"} />;
+      <hbr />
+      <ts.InterfaceMember name="member2" type="U" />;
+      <hbr />
+      <ts.InterfaceMember name="member3" type="V" />;
+      <hbr />
+      <ts.InterfaceMember name="member4" type="W" />;
+    </ts.InterfaceDeclaration>,
+  );
+
+  expect(res).toEqual(d`
+    interface Foo<T, U extends number, V = object, W extends string = "test"> {
+      member: T;
+      member2: U;
+      member3: V;
+      member4: W;
+    }
+  `);
+});
+
 describe("method members", () => {
   it("render basic", () => {
     expect(