Skip to content

[POC] Enable feature via directive#9886

Closed
timotheeguerin wants to merge 2 commits into
microsoft:mainfrom
timotheeguerin:feature-enable-directive
Closed

[POC] Enable feature via directive#9886
timotheeguerin wants to merge 2 commits into
microsoft:mainfrom
timotheeguerin:feature-enable-directive

Conversation

@timotheeguerin
Copy link
Copy Markdown
Member

@timotheeguerin timotheeguerin commented Mar 3, 2026

Feature Flags via Directives

Playground demo

Status

Proof of concept — code is 100% AI-generated and unreviewed. Demo only via playground/pkg.

Problem

When introducing new TypeSpec language features or behavior changes, there is no mechanism for users to incrementally opt in (or opt out) at the spec level. Today, features are either globally on or globally off based on the compiler version. This makes it difficult to:

  • Ship experimental or preview features behind a flag
  • Roll out breaking behavior changes gradually
  • Let users adopt new features at their own pace per-service

Proposal

Allow users to toggle named features using a directive-like statement in their TypeSpec source:

#enable "internal-modifier"
#disable "new-decorators"
@service
namespace MyService;

Semantics

  • Scope: A feature flag applies within the boundary of the file or namespace where it is declared. Recommended placement is at the top of the service definition.
  • Granularity: Each feature is identified by a string literal name (e.g. "internal-modifier").
  • Direction: #enable opts in; #disable opts out.
  • Defaults: Features define their own default (on or off). Flags override the default.

Open Questions

1. Is a directive the right syntax?

The # prefix is currently used for node-targeting directives (#suppress, #deprecated). Reusing # for statement-level directives that don't target a specific node may be confusing.

Alternative A: Double ## for statement-level directives

##enable "internal-modifier"
##disable "new-decorators"

Pros: Visually distinguishes statement-level directives from node-targeting ones.
Cons: Subtle difference (# vs ##) may be easy to miss.

Alternative B: Keyword-based syntax

enable "internal-modifier";
disable "new-decorators";

Pros: Reads naturally, consistent with other statement keywords.
Cons: enable/disable are not currently reserved keywords — reserving them would be a (limited) breaking change.

Alternative C: Keyword with explicit value

feature "internal-modifier" on;
feature "new-decorators" off;

Pros: Single keyword to reserve, explicit on/off value.
Cons: More verbose, on/off are not established patterns in TypeSpec.

Alternative D: Configuration in tspconfig.yaml

features:
  internal-modifier: on
  new-decorators: off

Pros: No new syntax needed, leverages existing configuration infrastructure. Clear separation between compiler configuration and spec authoring.
Cons: Cannot vary per-file or per-namespace — applies to the entire project. Less visible to someone reading the spec. Doesn't compose well if different parts of a project need different flags. Additionally, tspconfig.yaml currently serves as a build/output configuration rather than a project-level concept — there is no notion of a "tspconfig project" today, a single project may use multiple tspconfigs, and libraries don't read a tspconfig at all. Using it for feature flags would either require rethinking the role of tspconfig or accepting that flags wouldn't propagate through library boundaries.

2. Scope boundary

  • Should a feature flag in a file apply to just that file, the entire namespace, or the whole project?
  • Can a child namespace override a parent's flag?
  • What happens with conflicting flags across files in the same namespace?

3. Feature lifecycle

  • How are valid feature names registered and discovered? Should a library be allowed to define its own feature flags or only compiler?
  • What happens when a flag references an unknown feature name — warning, error, or silent ignore?
  • Should features have an expiration (e.g. removed once the behavior becomes the permanent default)?

4. Compiler integration

  • How does the compiler query whether a feature is enabled at a given source location?
  • Do library/emitter authors need an API to define and check feature flags?

Next Steps

  • Align on syntax choice
  • Define scoping rules
  • Design the compiler API for registering and querying feature flags
  • Validate with a real feature (e.g. internal-modifier)

@microsoft-github-policy-service microsoft-github-policy-service Bot added the compiler:core Issues for @typespec/compiler label Mar 3, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 3, 2026

Open in StackBlitz

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

commit: f3d407b

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 3, 2026

❌ There is undocummented changes. Run chronus add to add a changeset or click here.

The following packages have changes but are not documented.

  • @typespec/compiler
Show changes

@witemple-msft
Copy link
Copy Markdown
Member

Few thoughts:

  1. I'm not really sure we need a disable for features. I think it should probably be a one-way street. The only way I can see us needing a disable is if we were going to have a feature that is breaking to enable and then we could warn users to preemptively disable it. But, if it's breaking we can't enable it by default without deprecation notices and a major version bump anyway. So to me I don't see the lifecycle as being experimental -> default -> mandatory, but rather just disabled-by-default -> always-enabled. It just seems that having a bidirectional toggle adds complexity that probably doesn't buy us much.
  2. I wonder if there's room for something like a package-level directive. Right now, a # directive applies to the next syntax element. Rust has similar annotations and crate-level annotations. In Rust, #[annotation] means "the next statement has annotation applied (whatever that means)", but #![annotation] means "this crate has annotation applied." This seems to make the most sense to me for scoping. It seems like a "language feature" naturally would apply at the package scope, rather than the scope of a particular namespace or file. Similarly, I don't think the whole project is the right scope, because libraries should be able to independently decide if a feature is enabled in the scope of their control.
  3. There is a serious diamond dependency problem with allowing libraries to have their own feature flags separate from the compiler/language itself if two packages reach the same library with different expectations about feature enablement within that library.
  4. I think feature names should be static lists of allowable strings, and anything outside that set should error.

@timotheeguerin
Copy link
Copy Markdown
Member Author

For 2 isn't it what alternative A ## is? Open to a different combo though

@azure-sdk
Copy link
Copy Markdown
Collaborator

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@timotheeguerin
Copy link
Copy Markdown
Member Author

Design meeting:

  • leaning towards the tspconfig.yaml option pending a tspconfig project

@microsoft-github-policy-service microsoft-github-policy-service Bot added the stale Mark a PR that hasn't been recently updated and will be closed. label May 4, 2026
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 stale Mark a PR that hasn't been recently updated and will be closed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants