Skip to content

POC: ambient module declarations keyed on import attributes#4666

Open
bartlomieju wants to merge 3 commits into
microsoft:mainfrom
bartlomieju:feat/ambient-module-import-attributes
Open

POC: ambient module declarations keyed on import attributes#4666
bartlomieju wants to merge 3 commits into
microsoft:mainfrom
bartlomieju:feat/ambient-module-import-attributes

Conversation

@bartlomieju

Copy link
Copy Markdown

This is a proof-of-concept implementation of the syntax proposed in
microsoft/TypeScript#46135, ambient module declarations that are keyed on import
attributes:

declare module "*" with { type: "text" } {
  const data: string;
  export default data;
}
declare module "*" with { type: "bytes" } {
  const data: Uint8Array<ArrayBuffer>;
  export default data;
}

import text from "./file.txt" with { type: "text" };   // string
import bytes from "./file.txt" with { type: "bytes" };  // Uint8Array

It is a discussion-starter, the goal is to make the proposal concrete enough to argue
about precedence and matching rules on real code. I'm opening it against the Go port
because that is where I need it, but the design questions are language-level and
apply equally to the TypeScript compiler.

Motivation

Runtimes and bundlers (Deno, Bun, Node) already support
import x from "./f" with { type: "text" | "bytes" }, but TypeScript has no way
to type these imports. Today the only workaround is a custom CompilerHost
module-resolution hook, which is unavailable when a tool shells out to tsgo (or
tsc) as a standalone --project build. Deno tracks the missing type support in
denoland/deno#36124; microsoft/TypeScript#46135 is the sanctioned upstream
mechanism for expressing it in .d.ts. Extension-based ambients
(declare module "*.txt") get close, but they cannot distinguish two imports of
the same file by attribute, which is exactly what type: "text" vs
type: "bytes" needs. This has surfaced now, because we're removing our fork
of TypeScript, where we handled text/bytes imports explicitly as special cases.

What this does

  • Parser: a string-literal-named ambient module may carry a with { ... } clause
    between the name and the body, reusing the existing
    import-attributes parser. declare global is excluded.
  • AST: ModuleDeclaration gains an optional Attributes (ImportAttributes)
    member (schema plus regen).
  • Binder: an attribute-keyed ambient module is recorded on the source file in a
    new AttributedAmbientModules list and given a distinct symbol name
    ("*" with {type=text}), so it neither merges with nor shadows a plain
    declare module "*", and does not participate in normal pattern/ambient
    resolution.
  • Checker: when a module specifier carries import attributes,
    resolveExternalModule consults the attribute-keyed declarations before file
    resolution. A declaration matches when its attribute set exactly equals the
    import's attributes and its specifier pattern matches; pattern specificity
    reuses the existing "longest prefix before *" rule.
  • Printer: declaration emit round-trips the clause.

Precedence and semantics chosen (all up for debate)

  • An attribute-keyed ambient match wins over the specifier's own file resolution.
    This is what lets import x from "./real.ts" with { type: "text" } be typed by
    the ambient rather than by ./real.ts, and it is the capability plain
    extension ambients cannot provide. Whether the real module should sometimes win
    is the central open question (see #46135 and the precedence discussion in
    Implement Import Assertions (stage 3) TypeScript#40694, comment 918343758).
  • type: "json" is untouched: the built-in JSON typing path is unchanged, and an
    import whose attributes match no attribute-keyed declaration falls straight
    through to normal resolution. resolveJsonModule behavior is unaffected.
  • Attribute matching is exact set-equality on string-valued attributes to start
    with, as suggested in the issue.

Open questions for the discussion

  • Real-module precedence: should an attribute-keyed ambient always beat file
    resolution (this PR), or only when the specifier does not otherwise resolve, or
    only for non-relative specifiers? This is the crux of #46135.
  • Pattern specificity: this reuses pattern ambient module rules, where
    specificity is the prefix length before *. That means *.svg does not
    outrank * (both have an empty prefix), and the tie is resolved by declaration
    order, same as today's pattern ambients. Should trailing patterns like *.svg
    be made more specific?
  • resolveJsonModule interaction: should a user-written
    declare module "*" with { type: "json" } be allowed to override the built-in
    JSON typing, or should type: "json" remain reserved?
  • Scope: dynamic import("x", { with: { ... } }) and import-type nodes are not
    wired up yet; only static import/export declarations are. (Happy to update the
    PR to handle that too)

Tests

New conformance cases under tests/cases/conformance/importAttributes cover:
text vs bytes discrimination on the same file; an attribute-keyed ambient winning
over a real .ts module; type: "json" and a plain declare module "*" left
unaffected; pattern specificity; and declaration-emit round-trip. The existing
suite passes with no baseline changes.

Disclosure: I used AI (Claude and Codex) to research and implement this change, but I reviewed it manually before opening a PR.

Proof-of-concept for microsoft/TypeScript#46135: allow a `with { ... }` clause
on ambient module declarations, so the same file can be typed differently based
on an import's attributes:

    declare module "*" with { type: "text" }  { const d: string; export default d; }
    declare module "*" with { type: "bytes" } { const d: Uint8Array; export default d; }

    import text  from "./f.txt" with { type: "text" };   // string
    import bytes from "./f.txt" with { type: "bytes" };  // Uint8Array

- AST: optional Attributes member on ModuleDeclaration (schema + regen)
- Parser: parse the with/assert clause on string-literal ambient modules
- Binder: record attribute-keyed ambients separately with a distinct symbol name
  so they neither merge with nor shadow a plain `declare module "*"`
- Checker: consult attribute-keyed ambients during module resolution when the
  import carries matching attributes, before file resolution; type:"json" and
  attribute-less imports are unaffected
- Printer: round-trip the clause in declaration emit

Adds conformance tests for text/bytes discrimination, real-module override,
type:"json" left unaffected, pattern specificity, and declaration-emit round-trip.

Refs microsoft/TypeScript#46135, denoland/deno#36124
@bartlomieju

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

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.

Pull request overview

Adds proof-of-concept support for ambient module declarations selected by import attributes.

Changes:

  • Extends parsing, AST generation, printing, and declaration emit.
  • Adds binder/checker resolution for attribute-keyed ambient modules.
  • Adds conformance tests and baselines for matching, precedence, JSON, and emit.

Reviewed changes

Copilot reviewed 34 out of 37 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesTextAndBytes.ts Tests attribute-based type discrimination.
testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesPatternSpecificity.ts Tests wildcard specificity.
testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesOverridesRealModule.ts Tests precedence over resolved files.
testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesJsonUnaffected.ts Tests JSON and plain ambient behavior.
testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts Tests declaration round-tripping.
testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.types Type baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.symbols Symbol baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.errors.txt Diagnostic baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.types Specificity type baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.symbols Specificity symbol baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.types Precedence type baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.symbols Precedence symbol baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.errors.txt Precedence diagnostic baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.types JSON type baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.symbols JSON symbol baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.types Emit type baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.symbols Emit symbol baseline.
testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.js Declaration output baseline.
internal/transformers/declarations/transform.go Preserves attributes during declaration transforms.
internal/printer/printer.go Prints module import attributes.
internal/parser/reparser.go Updates module factory calls.
internal/parser/parser.go Parses attributed ambient modules.
internal/parser/jsdoc.go Updates JSDoc module construction.
internal/ls/codeactions_fixmissingtypeannotation.go Updates namespace construction.
internal/checker/nodebuilder_hover.go Updates hover module construction.
internal/checker/checker.go Resolves matching attributed ambients.
internal/binder/binder.go Records and uniquely binds attributed ambients.
internal/ast/utilities.go Adds attribute mapping and key generation.
internal/ast/ast.go Adds attributed ambient metadata.
internal/ast/ast_generated.go Adds module attributes to the Go AST.
internal/api/encoder/encoder_generated.go Encodes the new AST child.
internal/api/encoder/decoder_generated.go Decodes the new AST child.
_scripts/ast.json Extends the AST schema.
_packages/native-preview/src/ast/visitor.generated.ts Visits module attributes.
_packages/native-preview/src/ast/factory.generated.ts Creates and updates attributed modules.
_packages/native-preview/src/ast/ast.generated.ts Exposes module attributes.
_packages/native-preview/src/api/node/protocol.generated.ts Adds attributes to protocol children.
Files not reviewed (3)
  • internal/api/encoder/decoder_generated.go: Generated file
  • internal/api/encoder/encoder_generated.go: Generated file
  • internal/ast/ast_generated.go: Generated file

Comment thread internal/checker/checker.go
Comment thread internal/checker/checker.go Outdated
Comment thread internal/checker/checker.go Outdated
Comment thread internal/ast/utilities.go
Comment thread internal/ast/utilities.go Outdated
- Consult attribute-keyed ambients before the plain ambient module, so an
  attributed import wins over a plain `declare module` of the same specifier,
  not just over file resolution.
- Fix specificity so an exact attribute-keyed specifier is never overwritten by
  a later wildcard; an exact match now wins immediately.
- Correct the specificity doc comment (a trailing `*.ext` pattern does not
  outrank a bare `*`).
- Validate that attribute values on an ambient module declaration are string
  literals (TS2858), mirroring import/export attribute validation.
- Quote names and values in the attribute key so distinct attribute sets cannot
  collide into one symbol key (e.g. `{ a: "b,c=d" }` vs `{ a: "b", c: "d" }`).

Adds conformance tests for exact precedence, invalid attribute values, and
distinct attribute keys.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants