Skip to content

[compiler] Resolve members through template parameters.#9868

Open
witemple-msft wants to merge 19 commits intomicrosoft:mainfrom
witemple-msft:witemple-msft/templateparameter-metaproperties
Open

[compiler] Resolve members through template parameters.#9868
witemple-msft wants to merge 19 commits intomicrosoft:mainfrom
witemple-msft:witemple-msft/templateparameter-metaproperties

Conversation

@witemple-msft
Copy link
Member

This PR enables resolving member symbols (properties, metaproperties) through template parameters based on constraints.

It accomplishes this by adding a new Type node, TemplateParameterAccess, which is a semantic equivalent of a member expression where the base of the member access is a template parameter or another template parameter access expression. These are only visible inside the context of template declarations. Otherwise, like template parameters, they become "resolved" to concrete types when the template is instantiated.

This allows you to invoke metaproperties such as ::returnType when a template item is constrained to an Operation. This is recursive, so I.o::returnType is allowed if I is an interface that is proven to have an operation named o. proven is the keyword. Members only resolve if the constraint guarantees their presence.

This feature enables much more powerful templates. For example:

model Resource {
  @visibility(Lifecycle.Read)
  id: string;
}

@format("uuid")
scalar uuid extends string;

model MyResource extends Resource {
  @visibility(Lifecycle.Read)
  id: uuid;

  myProp: string;
}

@error
model Error {
  @minValue(400)
  @maxValue(599)
  @statusCode code: int32;
  message: string;
}

interface Ops<R extends Resource> {
  // Notice, we can actually use the type of the specific `id` property of this resource, enriched with all of its metadata,
  // because the constraint `Resource` guarantees it is present.
  get(@path id: R.id): Read<R> | Error;
  @post
  create(@bodyRoot resource: Create<R>): Read<R> | Error;
  // ...
}

interface MyOps extends Ops<MyResource> {}

⚠️ This PR was almost entirely generated by GitHub Copilot CLI.

@microsoft-github-policy-service microsoft-github-policy-service bot added compiler:core Issues for @typespec/compiler emitter:openapi3 Issues for @typespec/openapi3 emitter labels Mar 2, 2026
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/compiler@9868
npm i https://pkg.pr.new/@typespec/html-program-viewer@9868
npm i https://pkg.pr.new/@typespec/http-server-csharp@9868
npm i https://pkg.pr.new/@typespec/tspd@9868

commit: 4e9b6c6

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

All changed packages have been documented.

  • @typespec/compiler
  • @typespec/html-program-viewer
  • @typespec/http-server-csharp
  • @typespec/tspd
Show changes

@typespec/compiler - feature ✏️

Enabled resolution of member properties and metaproperties through template parameters based on constraints.,> ,> tsp,> model Resource {,> id: string;,> },> ,> model Read<R extends Resource> {,> id: R.id;,> },>

@typespec/html-program-viewer - internal ✏️

Updated some packages to account for introduction of new TemplateParameterAccess virtual type.

@typespec/http-server-csharp - internal ✏️

Updated some packages to account for introduction of new TemplateParameterAccess virtual type.

@typespec/tspd - internal ✏️

Updated some packages to account for introduction of new TemplateParameterAccess virtual type.

@witemple-msft witemple-msft added the int:azure-specs Run integration tests against azure-rest-api-specs label Mar 2, 2026
@azure-sdk
Copy link
Collaborator

azure-sdk commented Mar 2, 2026

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@witemple-msft
Copy link
Member Author

Solid illustration of functionality here (see ResourceOperations): Playground

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new TemplateParameterAccess type to the TypeSpec compiler, enabling member access (.) and meta-member access (::) through template parameters based on their constraints. For instance, if R extends Resource and Resource guarantees a property id, then R.id can be used as a type in template declarations.

Changes:

  • Adds a new TemplateParameterAccess internal type and comprehensive resolution logic in checker.ts and name-resolver.ts for resolving member/meta-member access through template parameter constraints
  • Updates all subsystems that handle the Type union (semantic walker, type relation checker, string template utils, type name utils, completions, hover, document highlight) to recognize the new type
  • Updates dependent packages (html-program-viewer, http-server-csharp, tspd) and adds tests for completions, hover, document highlighting, and reference resolution

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/compiler/src/core/types.ts Adds TemplateParameterAccess interface and includes it in the Type union
packages/compiler/src/core/checker.ts Core logic for resolving template parameter access (member & meta-member), caching, and completions
packages/compiler/src/core/name-resolver.ts Adds getMetaMemberNames API to expose available meta-member names for completion and validation
packages/compiler/src/core/semantic-walker.ts Adds navigation support for TemplateParameterAccess in the semantic walker
packages/compiler/src/core/type-relation-checker.ts Treats TemplateParameterAccess like TemplateParameter (uses constraint for assignability)
packages/compiler/src/core/helpers/type-name-utils.ts Returns path as the type name for TemplateParameterAccess
packages/compiler/src/core/helpers/string-template-utils.ts Handles TemplateParameterAccess in string template serialization check
packages/compiler/src/server/type-signature.ts Renders hover signature for TemplateParameterAccess as (template access)
packages/compiler/test/checker/references.test.ts Tests for resolving references through template parameter constraints
packages/compiler/test/checker/operations.test.ts Tests that operation parameters resolve correctly with template member types
packages/compiler/test/server/completion.test.ts Tests for IDE completions through constrained template parameters
packages/compiler/test/server/get-hover.test.ts Tests for hover information on template parameter access expressions
packages/compiler/test/server/document-highlight.test.ts Tests for document highlighting of template access references
packages/html-program-viewer/src/react/type-config.ts Registers TemplateParameterAccess in the program viewer UI
packages/http-server-csharp/src/lib/service.ts Handles TemplateParameterAccess as an unsupported type (returns undefined)
packages/tspd/src/ref-doc/utils/type-signature.ts Renders TemplateParameterAccess signature in documentation generation
.chronus/changes/*.md Two changeset entries for the compiler (feature) and dependent packages (internal)

/** @internal */
base: TemplateParameter | TemplateParameterAccess;
/** @internal */
path: string;
Copy link
Member

Choose a reason for hiding this comment

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

what does path corespond to? and cacheKey?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm still iterating on this and reviewing the agent's work. I'm a little suspicious of cacheKey myself, at least I feel it can be moved into checker state rather than placed on the Type itself, or potentially refactored into a clearer form of associativity.

The idea for path is that it caches the string that is shown to users in hover texts etc. so that it doesn't have to be recursively reconstructed N times per referent. That creates a potential exponential explosion problem (similar to naive fibonnaci or factorial) of string concatenations where many nodes can reference the same base, and then that base would have to compute its base paths N^2 times, so on.

The idea of cacheKey is basically this as I understand it:

  • There is no declared symbol for a template parameter access that the binder provides, but we need a Sym for type/value resolution pipelines in the checker, so the checker creates a synthetic late-bound symbol for template parameter access at check-time and sets sym.type to the template parameter access node.
  • Example: T.id::type, T.id gets a synthetic late-bound Sym. That Sym stands in for "the result of accessing id on T even though there isn't a concrete property declaration on T yet.
  • The cacheKey prevents many symbols from being created for the same access. If we created a fresh late-bound symbol every time we saw the same logical chain, then two different instances of T.id would get different symbol identities, which would not cache coherently and they would not have stable symbol identity. Chained accesses would keep re-allocating intermediate results for the same logical thing.
  • The cache makes it so that for the same root template parameter instance by identity and logical access chain, the same synthetic symbol is reused so it is coherent across the program.
  • The cache is only used when there is no mapper, because using the cache when a mapper is active would smear together multiple different meanings of T.id under different concrete type mapper contexts into the same Sym. So during instantiation, even partial instantiation, the cache is not used so that new symbols are created reflecting particular instances of the template parameter access.

I don't really think either of these things has to live on the Type itself, and I'm inclined to at least move the cacheKey off into a weakmap. The path, though, could be useful to have on the type so that things that observe the uninstantiated Type don't have to go mucking around in the checker to compute a display string for it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved cacheKey to checker state.

* @param baseEntity Template parameter or prior template access chain.
* @returns The resolved member/meta-member type, or `errorType` when not guaranteed.
*/
function resolveTemplateAccessType(
Copy link
Member

Choose a reason for hiding this comment

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

would that work for values?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not currently. No extant access patterns can resolve to values, since it's just property references and metaproperties which are all types. The proposed ::name metaproperty would be an example of one that can resolve to a value (I think technically it should resolve to an indeterminate for accuracy). I'd rather just handle types here and deal with this when they can resolve to values, since it may involve additional machinery to make that work correctly.

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 emitter:openapi3 Issues for @typespec/openapi3 emitter int:azure-specs Run integration tests against azure-rest-api-specs tspd Issues for the tspd tool ui:type-graph-viewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants