diff --git a/packages/graphql/src/components/types/index.ts b/packages/graphql/src/components/types/index.ts
index f0c8f56e711..9d53ec9d26d 100644
--- a/packages/graphql/src/components/types/index.ts
+++ b/packages/graphql/src/components/types/index.ts
@@ -1,3 +1,6 @@
export { ScalarType, type ScalarTypeProps } from "./scalar-type.js";
export { EnumType, type EnumTypeProps } from "./enum-type.js";
export { UnionType, type UnionTypeProps } from "./union-type.js";
+export { InterfaceType, type InterfaceTypeProps } from "./interface-type.js";
+export { ObjectType, type ObjectTypeProps } from "./object-type.js";
+export { InputType, type InputTypeProps } from "./input-type.js";
diff --git a/packages/graphql/src/components/types/input-type.tsx b/packages/graphql/src/components/types/input-type.tsx
new file mode 100644
index 00000000000..39acc731e4d
--- /dev/null
+++ b/packages/graphql/src/components/types/input-type.tsx
@@ -0,0 +1,36 @@
+import { type Model, getDoc } from "@typespec/compiler";
+import * as gql from "@alloy-js/graphql";
+import { useTsp } from "@typespec/emitter-framework";
+import { useGraphQLSchema } from "../../context/index.js";
+import { Field } from "../fields/index.js";
+
+export interface InputTypeProps {
+ /** The input type to render */
+ type: Model;
+}
+
+/**
+ * Renders a GraphQL input type declaration
+ *
+ * Determines the correct input type name:
+ * - If the model is also an output type, appends "Input"
+ * - Otherwise, uses the name as-is
+ */
+export function InputType(props: InputTypeProps) {
+ const { program } = useTsp();
+ const { modelVariants } = useGraphQLSchema();
+ const doc = getDoc(program, props.type);
+ const properties = Array.from(props.type.properties.values());
+
+ // If there's an output variant with the same name, add Input suffix
+ const hasOutputVariant = modelVariants.outputModels.has(props.type.name);
+ const inputTypeName = hasOutputVariant ? `${props.type.name}Input` : props.type.name;
+
+ return (
+
+ {properties.map((prop) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/graphql/src/components/types/interface-type.tsx b/packages/graphql/src/components/types/interface-type.tsx
new file mode 100644
index 00000000000..ef268baa9b5
--- /dev/null
+++ b/packages/graphql/src/components/types/interface-type.tsx
@@ -0,0 +1,28 @@
+import { type Model, getDoc } from "@typespec/compiler";
+import * as gql from "@alloy-js/graphql";
+import { useTsp } from "@typespec/emitter-framework";
+import { Field } from "../fields/index.js";
+
+export interface InterfaceTypeProps {
+ /** The interface type to render */
+ type: Model;
+}
+
+/**
+ * Renders a GraphQL interface type declaration
+ *
+ * Interfaces are marked with @Interface decorator in TypeSpec
+ */
+export function InterfaceType(props: InterfaceTypeProps) {
+ const { program } = useTsp();
+ const doc = getDoc(program, props.type);
+ const properties = Array.from(props.type.properties.values());
+
+ return (
+
+ {properties.map((prop) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/graphql/src/components/types/object-type.tsx b/packages/graphql/src/components/types/object-type.tsx
new file mode 100644
index 00000000000..270e63fd97b
--- /dev/null
+++ b/packages/graphql/src/components/types/object-type.tsx
@@ -0,0 +1,48 @@
+import { type Model, getDoc } from "@typespec/compiler";
+import * as gql from "@alloy-js/graphql";
+import { useTsp } from "@typespec/emitter-framework";
+import { Field, OperationField } from "../fields/index.js";
+import { getComposition } from "../../lib/interface.js";
+import { getOperationFields } from "../../lib/operation-fields.js";
+
+export interface ObjectTypeProps {
+ /** The object type to render */
+ type: Model;
+}
+
+/**
+ * Renders a GraphQL object type declaration
+ *
+ * Handles:
+ * - Regular fields from model properties
+ * - Interface implementations via @compose
+ * - Operation fields via @operationFields
+ */
+export function ObjectType(props: ObjectTypeProps) {
+ const { program } = useTsp();
+ const doc = getDoc(program, props.type);
+ const properties = Array.from(props.type.properties.values());
+ const implementations = getComposition(program, props.type);
+ const operationFields = getOperationFields(program, props.type);
+
+ // Convert interface implementations to string references.
+ // Note: getComposition returns original (pre-mutation) models from decorator state.
+ // This works because the mutation engine doesn't rename models, so iface.name
+ // matches the name used by InterfaceType for the same model.
+ const implementsRefs = implementations?.map((iface) => iface.name) || [];
+
+ return (
+
+ {properties.map((prop) => (
+
+ ))}
+ {Array.from(operationFields).map((op) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/graphql/test/components/input-type.test.tsx b/packages/graphql/test/components/input-type.test.tsx
new file mode 100644
index 00000000000..cfc40279701
--- /dev/null
+++ b/packages/graphql/test/components/input-type.test.tsx
@@ -0,0 +1,87 @@
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it, beforeEach } from "vitest";
+import { InputType } from "../../src/components/types/index.js";
+import { Tester } from "../test-host.js";
+import { renderComponentToSDL } from "./component-test-utils.js";
+
+describe("InputType component", () => {
+ let tester: Awaited>;
+ beforeEach(async () => {
+ tester = await Tester.createInstance();
+ });
+
+ it("renders a basic input type with fields", async () => {
+ const { CreateUser } = await tester.compile(
+ t.code`model ${t.model("CreateUser")} { name: string; email: string; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("input CreateUser {");
+ expect(sdl).toContain("name: String!");
+ expect(sdl).toContain("email: String!");
+ });
+
+ it("renders with doc comment description", async () => {
+ const { LoginInput } = await tester.compile(
+ t.code`
+ /** Credentials for login */
+ model ${t.model("LoginInput")} { username: string; password: string; }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("Credentials for login");
+ expect(sdl).toContain("input LoginInput {");
+ });
+
+ it("renders optional fields as non-null (GraphQL input convention)", async () => {
+ // In GraphQL, input fields are always non-null; optionality is expressed
+ // via default values, not nullability. Only `| null` makes them nullable.
+ const { UpdateUser } = await tester.compile(
+ t.code`model ${t.model("UpdateUser")} { name?: string; bio?: string; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("name: String!");
+ expect(sdl).toContain("bio: String!");
+ });
+
+ it("appends Input suffix when model has an output variant", async () => {
+ const { Pet } = await tester.compile(
+ t.code`model ${t.model("Pet")} { name: string; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, , {
+ modelVariants: {
+ outputModels: new Map([["Pet", Pet]]),
+ inputModels: new Map([["Pet", Pet]]),
+ },
+ });
+
+ expect(sdl).toContain("input PetInput {");
+ });
+
+ it("uses original name when no output variant exists", async () => {
+ const { CreatePet } = await tester.compile(
+ t.code`model ${t.model("CreatePet")} { name: string; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("input CreatePet {");
+ expect(sdl).not.toContain("CreatePetInput");
+ });
+
+ it("renders array fields as list types", async () => {
+ const { TagInput } = await tester.compile(
+ t.code`model ${t.model("TagInput")} { values: string[]; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("values: [String!]!");
+ });
+});
diff --git a/packages/graphql/test/components/interface-type.test.tsx b/packages/graphql/test/components/interface-type.test.tsx
new file mode 100644
index 00000000000..904e6cdf77a
--- /dev/null
+++ b/packages/graphql/test/components/interface-type.test.tsx
@@ -0,0 +1,75 @@
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it, beforeEach } from "vitest";
+import { InterfaceType } from "../../src/components/types/index.js";
+import { Tester } from "../test-host.js";
+import { renderComponentToSDL } from "./component-test-utils.js";
+
+describe("InterfaceType component", () => {
+ let tester: Awaited>;
+ beforeEach(async () => {
+ tester = await Tester.createInstance();
+ });
+
+ it("renders a basic interface with fields", async () => {
+ const { Node } = await tester.compile(
+ t.code`
+ @Interface
+ model ${t.model("Node")} { id: string; }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("interface Node {");
+ expect(sdl).toContain("id: String!");
+ });
+
+ it("renders with doc comment description", async () => {
+ const { Entity } = await tester.compile(
+ t.code`
+ /** A base entity */
+ @Interface
+ model ${t.model("Entity")} { id: string; }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("A base entity");
+ expect(sdl).toContain("interface Entity {");
+ });
+
+ it("renders multiple fields with correct types", async () => {
+ const { Timestamped } = await tester.compile(
+ t.code`
+ @Interface
+ model ${t.model("Timestamped")} {
+ createdAt: string;
+ updatedAt: string;
+ version: int32;
+ }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("interface Timestamped {");
+ expect(sdl).toContain("createdAt: String!");
+ expect(sdl).toContain("updatedAt: String!");
+ expect(sdl).toContain("version: Int!");
+ });
+
+ it("renders optional fields as nullable", async () => {
+ const { Described } = await tester.compile(
+ t.code`
+ @Interface
+ model ${t.model("Described")} { description?: string; }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("description: String");
+ expect(sdl).not.toContain("description: String!");
+ });
+});
diff --git a/packages/graphql/test/components/object-type.test.tsx b/packages/graphql/test/components/object-type.test.tsx
new file mode 100644
index 00000000000..79b73c01759
--- /dev/null
+++ b/packages/graphql/test/components/object-type.test.tsx
@@ -0,0 +1,122 @@
+import { t } from "@typespec/compiler/testing";
+import * as gql from "@alloy-js/graphql";
+import { describe, expect, it, beforeEach } from "vitest";
+import { ObjectType } from "../../src/components/types/index.js";
+import { Tester } from "../test-host.js";
+import { renderComponentToSDL } from "./component-test-utils.js";
+
+describe("ObjectType component", () => {
+ let tester: Awaited>;
+ beforeEach(async () => {
+ tester = await Tester.createInstance();
+ });
+
+ it("renders a basic object type with fields", async () => {
+ const { User } = await tester.compile(
+ t.code`model ${t.model("User")} { name: string; age: int32; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("type User {");
+ expect(sdl).toContain("name: String!");
+ expect(sdl).toContain("age: Int!");
+ });
+
+ it("renders with doc comment description", async () => {
+ const { Item } = await tester.compile(
+ t.code`
+ /** A store item */
+ model ${t.model("Item")} { title: string; }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("A store item");
+ expect(sdl).toContain("type Item {");
+ });
+
+ it("renders optional fields as nullable", async () => {
+ const { Profile } = await tester.compile(
+ t.code`model ${t.model("Profile")} { bio?: string; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("bio: String");
+ expect(sdl).not.toContain("bio: String!");
+ });
+
+ it("renders array fields as list types", async () => {
+ const { Post } = await tester.compile(
+ t.code`model ${t.model("Post")} { tags: string[]; }`,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("tags: [String!]!");
+ });
+
+ it("renders field with doc comment description", async () => {
+ const { Thing } = await tester.compile(
+ t.code`
+ model ${t.model("Thing")} {
+ /** The display name */
+ name: string;
+ }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("The display name");
+ expect(sdl).toContain("name: String!");
+ });
+
+ it("renders deprecated fields", async () => {
+ const { Entry } = await tester.compile(
+ t.code`
+ model ${t.model("Entry")} {
+ current: string;
+ #deprecated "use current instead"
+ old: string;
+ }
+ `,
+ );
+
+ const sdl = renderComponentToSDL(tester.program, );
+
+ expect(sdl).toContain("current: String!");
+ expect(sdl).toContain("old: String!");
+ expect(sdl).toContain("@deprecated");
+ expect(sdl).toContain("use current instead");
+ });
+
+ it("renders with interface implementation via @compose", async () => {
+ const { Pet } = await tester.compile(
+ t.code`
+ @Interface
+ model ${t.model("Node")} { id: string; }
+
+ @compose(Node)
+ model ${t.model("Pet")} { ...Node; name: string; }
+ `,
+ );
+
+ // Register the Node interface in the schema so buildSchema can resolve it
+ const sdl = renderComponentToSDL(
+ tester.program,
+ <>
+
+
+
+
+ >,
+ );
+
+ expect(sdl).toContain("type Pet implements Node {");
+ expect(sdl).toContain("id: String!");
+ expect(sdl).toContain("name: String!");
+ });
+});