From be76dfab234e9d3a10cae795f3272e2a99ca6568 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:53:55 +0000 Subject: [PATCH 1/2] Initial plan From 7903666839f1ea604c371b392b66e332e3c7f3a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:58:38 +0000 Subject: [PATCH 2/2] feat(compiler): add interface typekit Co-authored-by: witemple-msft <77019085+witemple-msft@users.noreply.github.com> --- ...d-interface-typekit-2026-02-23-17-55-00.md | 7 ++ packages/compiler/src/typekit/index.ts | 2 + packages/compiler/src/typekit/kits/index.ts | 1 + .../compiler/src/typekit/kits/interface.ts | 81 +++++++++++++++++++ .../compiler/test/typekit/interface.test.ts | 62 ++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 .chronus/changes/copilot-add-interface-typekit-2026-02-23-17-55-00.md create mode 100644 packages/compiler/src/typekit/kits/interface.ts create mode 100644 packages/compiler/test/typekit/interface.test.ts diff --git a/.chronus/changes/copilot-add-interface-typekit-2026-02-23-17-55-00.md b/.chronus/changes/copilot-add-interface-typekit-2026-02-23-17-55-00.md new file mode 100644 index 00000000000..58b1b06e152 --- /dev/null +++ b/.chronus/changes/copilot-add-interface-typekit-2026-02-23-17-55-00.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +compiler - Add typekit for interfaces (`$.interface`) diff --git a/packages/compiler/src/typekit/index.ts b/packages/compiler/src/typekit/index.ts index 63bf7279a8d..9fd0211a2da 100644 --- a/packages/compiler/src/typekit/index.ts +++ b/packages/compiler/src/typekit/index.ts @@ -11,6 +11,8 @@ export { EnumKit, EnumMemberDescriptor, EnumMemberKit, + InterfaceDescriptor, + InterfaceKit, IntrinsicKit, LiteralKit, ModelDescriptor, diff --git a/packages/compiler/src/typekit/kits/index.ts b/packages/compiler/src/typekit/kits/index.ts index e5bbf8de982..32a6018b8c9 100644 --- a/packages/compiler/src/typekit/kits/index.ts +++ b/packages/compiler/src/typekit/kits/index.ts @@ -3,6 +3,7 @@ export * from "./builtin.js"; export * from "./entity.js"; export * from "./enum-member.js"; export * from "./enum.js"; +export * from "./interface.js"; export * from "./intrinsic.js"; export * from "./literal.js"; export * from "./model-property.js"; diff --git a/packages/compiler/src/typekit/kits/interface.ts b/packages/compiler/src/typekit/kits/interface.ts new file mode 100644 index 00000000000..75d7720bb6d --- /dev/null +++ b/packages/compiler/src/typekit/kits/interface.ts @@ -0,0 +1,81 @@ +import type { Entity, Interface, Operation } from "../../core/types.js"; +import { createRekeyableMap } from "../../utils/misc.js"; +import { defineKit } from "../define-kit.js"; +import { decoratorApplication, DecoratorArgs } from "../utils.js"; + +/** + * A descriptor for creating an interface. + */ +export interface InterfaceDescriptor { + /** + * The name of the interface declaration. + */ + name: string; + + /** + * Decorators to apply to the interface. + */ + decorators?: DecoratorArgs[]; + + /** + * The operations of the interface. + */ + operations?: Operation[]; +} + +/** + * Utilities for working with interfaces. + * @typekit interface + */ +export interface InterfaceKit { + /** + * Create an interface type. + * + * @param desc The descriptor of the interface. + */ + create(desc: InterfaceDescriptor): Interface; + + /** + * Check if the given `type` is an interface. + * + * @param type The type to check. + */ + is(type: Entity): type is Interface; +} + +interface TypekitExtension { + /** + * Utilities for working with interfaces. + */ + interface: InterfaceKit; +} + +declare module "../define-kit.js" { + interface Typekit extends TypekitExtension {} +} + +defineKit({ + interface: { + create(desc) { + const iface: Interface = this.program.checker.createType({ + kind: "Interface", + name: desc.name, + decorators: decoratorApplication(this, desc.decorators), + operations: createRekeyableMap(), + sourceInterfaces: [], + }); + + for (const op of desc.operations ?? []) { + op.interface = iface; + iface.operations.set(op.name, op); + } + + this.program.checker.finishType(iface); + return iface; + }, + + is(type) { + return type.entityKind === "Type" && type.kind === "Interface"; + }, + }, +}); diff --git a/packages/compiler/test/typekit/interface.test.ts b/packages/compiler/test/typekit/interface.test.ts new file mode 100644 index 00000000000..5c00f703e08 --- /dev/null +++ b/packages/compiler/test/typekit/interface.test.ts @@ -0,0 +1,62 @@ +import { expect, it } from "vitest"; +import { $ } from "../../src/typekit/index.js"; +import { createContextMock, getTypes } from "./utils.js"; + +it("can check if a type is an Interface", async () => { + const { + Foo, + context: { program }, + } = await getTypes( + ` + interface Foo {}; + `, + ["Foo"], + ); + + expect($(program).interface.is(Foo)).toBe(true); +}); + +it("returns false when the type is not an interface", async () => { + const { + Foo, + context: { program }, + } = await getTypes( + ` + model Foo {}; + `, + ["Foo"], + ); + + expect($(program).interface.is(Foo)).toBe(false); + expect($(program).interface.is($(program).value.create("foo"))).toBe(false); +}); + +it("creates a new Interface", async () => { + const { program } = await createContextMock(); + const iface = $(program).interface.create({ + name: "Foo", + }); + + expect($(program).interface.is(iface)).toBe(true); + expect(iface.name).toBe("Foo"); + expect(iface.operations.size).toBe(0); +}); + +it("creates an Interface with operations", async () => { + const { program } = await createContextMock(); + + const op = $(program).operation.create({ + name: "myOp", + parameters: [], + returnType: $(program).intrinsic.void, + }); + + const iface = $(program).interface.create({ + name: "Foo", + operations: [op], + }); + + expect(iface.operations.size).toBe(1); + expect(iface.operations.get("myOp")).toBe(op); + expect(op.interface).toBe(iface); +});