Skip to content

Adding partial keyword support to interface. - #11948

Open
Gerardo Lecaros (glecaros) wants to merge 3 commits into
mainfrom
glecaros/partial
Open

Adding partial keyword support to interface.#11948
Gerardo Lecaros (glecaros) wants to merge 3 commits into
mainfrom
glecaros/partial

Conversation

@glecaros

@glecaros Gerardo Lecaros (glecaros) commented Sep 11, 2026

Copy link
Copy Markdown
Member

Summary

Add a partial modifier for interface declarations so that a single logical interface can be
split across multiple declarations — in the same file or across files — and have them combined
into one Interface type during compilation, as long as every declaration of that interface is
marked partial.

This is a common pattern in other languages (e.g. C#'s partial class) and is useful in TypeSpec
for splitting large interfaces across files, letting generated/scaffolded operations live alongside
hand-authored ones, or letting multiple libraries/specs contribute operations to a shared interface.

Motivating example

// operations-a.tsp
partial interface Widgets {
  list(): Widget[];
  read(id: string): Widget;
}

// operations-b.tsp
partial interface Widgets {
  create(widget: Widget): Widget;
  delete(id: string): void;
}

Both declarations combine into a single Widgets interface with all four operations, decorators,
and extends clauses merged.

Proposed behavior

  • partial is a new contextual modifier keyword, valid only on interface declarations (alongside
    the existing internal modifier). Using it on any other declaration kind is a compile error.
  • If any declaration of an interface with a given name is partial, all declarations of that
    name (same file or across files) must be partial. Mixing partial and non-partial declarations of
    the same interface name produces a clear diagnostic (partial-interface-mismatch) rather than a
    generic duplicate-symbol error.
  • Merging happens at both the binder level (same file) and the name-resolver level (across files),
    so partial interface Foo declared in two different .tsp files in the same namespace still
    merges into one Interface type.
  • Operations from every partial declaration are combined into a single operation set. Two partial
    declarations that redeclare the same operation name still produce the existing
    interface-duplicate diagnostic — partial declarations don't relax that check.
  • extends clauses from every partial declaration are combined.
  • Inline decorators (e.g. @doc("...")) on each partial declaration apply independently, exactly as
    written — this matches normal TypeSpec decorator semantics (last decorator processed wins for
    decorators like @doc), and lets each declaration legitimately contribute its own decorators.
  • Augment decorators (@@dec(Foo, ...)) and the doc-comment-derived decorator are applied to the
    merged interface exactly once, regardless of how many partial declarations exist, since they
    target the shared underlying symbol rather than a specific declaration node.
  • Decorators that self-enforce "only once per declaration" (via the compiler's
    validateDecoratorUniqueOnNode helper, e.g. @doc) correctly detect a duplicate even when the two
    applications are on two different partial declarations of the same interface — consistent with
    writing the decorator twice on a single non-partial declaration.
  • partial interface Foo<T> { ... } (templated partial interfaces) is rejected with a dedicated
    diagnostic (partial-interface-template); templates aren't supported for the POC.
  • A single, standalone partial interface Foo { ... } (with no other declarations to merge with) is
    allowed and behaves like a normal interface.

Scope of this change / POC

This is a proof-of-concept limited to interface declarations. It touches:

  • Scanner/parser: new partial keyword token and modifier parsing.
  • Binder: same-file merging of partial interface declarations into one symbol.
  • Name resolver: cross-file merging (including merging each declaration's member symbol
    tables) so operations from every file are visible together.
  • Checker: combining modifiers, decorators, extends, and operations across all partial
    declarations when building the Interface type; new diagnostics for mismatched/templated partial
    declarations.
  • Formatter, completion, syntax highlighting (monarch): support for the new keyword.

Not yet explored: partial on model, namespace re-opening semantics beyond what already exists,
or any interaction with template instantiation.

Testing

The POC includes unit test coverage for: same-file and cross-file merging, decorator combination
and deduplication (augment decorators, doc comments, and per-declaration inline decorators),
extends combination, duplicate-operation-name diagnostics, mismatched-partial diagnostics (same
file and cross-file), the templated-partial-interface diagnostic, the duplicate-decorator diagnostic
across partial declarations, and rejection of partial on non-interface declarations. Parser,
scanner, completion, and monarch (syntax highlighting) tests were also added/updated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate issues affect inheritance, decorator handling, and modifier validation.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds partial interface support across the compiler, tooling, formatter, and syntax highlighting.

Changes:

  • Adds parsing, AST, modifier, completion, formatting, and Monarch support.
  • Merges partial interfaces across files, including operations, decorators, and inheritance.
  • Adds diagnostics, tests, and a changeset.

Unresolved findings include a critical crash path, incomplete cross-file inheritance merging, decorator uniqueness issues, and overly broad modifier validation.

File summaries
File Summary
packages/monarch/test/typespec-monarch.test.ts Tests partial syntax highlighting.
packages/monarch/src/typespec-monarch.ts Adds Monarch keyword support.
packages/compiler/test/server/completion.test.ts Tests partial completion.
packages/compiler/test/scanner.test.ts Tests keyword tokenization.
packages/compiler/test/parser.test.ts Tests parsing and modifier usage.
packages/compiler/test/formatter/scenarios/outputs/interface.tsp Adds expected formatter output.
packages/compiler/test/formatter/scenarios/inputs/interface.tsp Adds formatter input coverage.
packages/compiler/test/checker/interface.test.ts Tests partial-interface checking.
packages/compiler/src/server/completion.ts Adds completion support.
packages/compiler/src/formatter/print/printer.ts Formats partial.
packages/compiler/src/core/types.ts Adds syntax and modifier types.
packages/compiler/src/core/scanner.ts Defines the keyword token.
packages/compiler/src/core/parser.ts Parses the modifier.
packages/compiler/src/core/name-resolver.ts Merges cross-file symbols.
packages/compiler/src/core/modifiers.ts Registers modifier compatibility.
packages/compiler/src/core/messages.ts Adds diagnostics.
packages/compiler/src/core/decorator-utils.ts Handles decorator uniqueness.
packages/compiler/src/core/checker.ts Checks merged interfaces.
packages/compiler/src/core/binder.ts Binds partial declarations.
.chronus/changes/partial-interface-2026-9-9-19-30-0.md Documents the feature.
Review details

Suppressed comments (1)

packages/compiler/src/core/decorator-utils.ts:320

  • ownerNodes now includes every declaration on the symbol, so this changes validateDecoratorUniqueOnNode for existing merged namespaces as well as partial interfaces. For example, two namespace declarations with one @doc each now satisfy sameDecorators.length > 1 and report duplicate-decorator, even though initializeTypeForNamespace intentionally applies decorators from each namespace declaration separately (checker.ts:2513-2518). Restrict the cross-declaration lookup to partial interfaces or preserve the previous behavior for other merged symbols.
  const ownerNodes: readonly Node[] = type.node?.symbol
    ? type.node.symbol.declarations
    : type.node
      ? [type.node]
  • Files reviewed: 20/20 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/compiler/src/core/checker.ts
Comment thread packages/compiler/src/core/decorator-utils.ts
Comment thread packages/compiler/src/core/name-resolver.ts
Comment thread packages/compiler/src/core/types.ts
Copilot AI review requested due to automatic review settings September 12, 2026 00:55
@pkg-pr-new

pkg-pr-new Bot commented Sep 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/compiler@11948

commit: 8c02f2f

@github-actions

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/compiler
Show changes

@typespec/compiler - feature ✏️

Add support for partial interfaces. A partial interface can be declared multiple times, including across different files, and every matching declaration must be marked partial. All operations, decorators, and extends clauses from each declaration are combined into a single interface.,> ,> typespec,> // a.tsp,> partial interface Widgets {,> list(): void;,> },> ,> // b.tsp,> partial interface Widgets {,> read(id: string): void;,> },>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in syntax-kind stability, modifier propagation, diagnostics, template validation, and decorator handling.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (5)

packages/compiler/src/core/binder.ts:644

  • The added tests cover root-level partial interfaces, but not the new declareNamespaceMember path at this merge hook. A regression for partial interfaces nested in a named namespace—especially cross-file merging through recursive namespace export tables—would pass all current tests; add same-file and cross-file namespace cases that assert the merged operations and mismatch diagnostics.
    if (
      flags & SymbolFlags.Interface &&
      mergePartialInterfaceDeclarations(node as InterfaceStatementNode, scope)
    ) {

packages/compiler/src/core/binder.ts:724

  • Because existingIsPartial is false for an ordinary interface, this condition also reports partial-interface-mismatch for interface Foo {}; interface Foo {};. That diagnostic is only for a mixed partial/non-partial set; let the all-non-partial case use the existing duplicate-symbol path.
    if (!isPartial || !existingIsPartial) {

packages/compiler/src/core/checker.ts:7818

  • The merged-declaration validation is after the links.declaredType early return. If partial interface Foo {} is declared first and a later partial declaration has template parameters, the first check caches the type and the later call returns before this loop, so the required partial-interface-template diagnostic is missed. Validate all declaration template parameters before the cached-type return.
      for (const declNode of declarations) {
        checkModifiers(program, declNode);
        if (
          declNode.modifierFlags & ModifierFlags.Partial &&
          declNode.templateParameters.length > 0
        ) {

packages/compiler/src/core/name-resolver.ts:1310

  • This else branch is also selected when both sourceBinding and targetBinding contain only non-partial interfaces, so duplicate interface declarations across files gain an incorrect partial-interface-mismatch in addition to duplicate-symbol. Only report the new diagnostic when at least one side contains a partial declaration; otherwise preserve normal duplicate handling.
    } else {
      program.reportDiagnostic(
        createDiagnostic({
          code: "partial-interface-mismatch",
          format: { name: key },
          target: sourceBinding.declarations[0] ?? getSymNode(sourceBinding),

packages/compiler/test/checker/interface.test.ts:479

  • The added cross-file test only merges top-level Foo; it does not exercise the recursive namespace path in name-resolver.ts:1222-1226, even though the PR promises partial interfaces in the same namespace across files. Add a two-file test with both declarations inside a namespace so a regression in recursive symbol-table merging is caught.
  it("combines operations from multiple partial declarations across files", async () => {
    const [{ Foo }, diagnostics] = await Tester.files({
      "other.tsp": `
          partial interface Foo {
            b(): void;
          }
        `,
    }).compileAndDiagnose(t.code`
      import "./other.tsp";

      partial interface ${t.interface("Foo")} {
        a(): void;
      }
      `);
    expectDiagnosticEmpty(diagnostics);
    deepStrictEqual([...Foo.operations.keys()].sort(), ["a", "b"]);
  • Files reviewed: 20/20 changed files
  • Comments generated: 4
  • Review effort level: Lite

InternalKeyword,
AutoKeyword,
FunctionTypeExpression,
PartialKeyword,
Comment on lines +737 to +739
// we have an existing binding, so just push this node to its declarations
mutate(existingBinding.declarations).push(node);
mutate(node).symbol = existingBinding;
Comment on lines +317 to +321
const ownerNodes: readonly Node[] = type.node?.symbol
? type.node.symbol.declarations
: type.node
? [type.node]
: [];
Comment on lines +1295 to +1298
if (allDeclarationsArePartial(sourceBinding) && allDeclarationsArePartial(targetBinding)) {
mergedSymbols.set(sourceBinding, targetBinding);
mutate(targetBinding.declarations).push(...sourceBinding.declarations);
// Combine the operations declared in each partial declaration into a single
@azure-sdk-automation

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:core Issues for @typespec/compiler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants