How TypeScript 7.1 checks embedded code
Content mappers give TypeScript virtual code to check, but source positions and permission to execute the mapper determine how useful and safe that check is.
TypeScript 7.1.0-beta can report an undefined name inside an HTML event handler without parsing the surrounding HTML. In the example below, it points to line 5, column 34. The same run also reports an error in a generated function with no corresponding source location. Both results come from the same mapper.
The TypeScript 7.1 beta announcement, dated October 20, 2026, introduces content mappers. These external programs turn otherwise unsupported files into virtual code and supply mappings back to the original text. The mechanism connects command-line checking and editor support for formats such as Vue, Svelte, Astro and MDX. The announcement describes their integrations as prototypes or implementations.
The check is only useful if its errors and editor edits point back to the right source. The HTML example below tests source locations and exposes a generated-code error. Enabling its mapper also grants permission to execute code.
The checker reads a generated file
The obvious approach is to extract a script block and feed it to TypeScript. That gives the checker valid input, but its locations describe the extracted text. Remove an opening tag and the offsets change. Decode an HTML character reference inside an attribute and even a fixed offset stops working.
The implementation and protocol write-up documents this process. TypeScript discovers files with registered extensions through project inclusion or imports. It starts the mapper described by the package manifest, then requests transformations over JSON-RPC on standard input and output.
The mapper returns parseable virtual text, its extension and span mappings. It can also return supplemental virtual files, for example one for each script block. TypeScript checks that output using its existing checker, then uses the mappings to translate diagnostics and language-service results.
Follow the diagnostic through this simplified flow. The span map takes a separate path from the generated code.
The checker sees the virtual code, while the diagnostic renderer needs both outputs. The diagram omits project initialization and supplemental files. An error in generated code without a mapping has no original location, as the next example demonstrates.
One run exposes both kinds of error
The upstream HTML sample uses the extension .dhtml. It extracts inline scripts and event-handler attributes. It does not load external scripts referenced with src. The following run uses TypeScript 7.1.0-beta, Node.js 26.12.0 on Linux, and dhtml-content-mapper 0.1.0 at commit 2608e487731056ecd981a48ddcb286991694c8bf.
This is the complete project configuration used for the run. contentMappers is a top-level property, outside compilerOptions.
{ "compilerOptions": { "allowJs": true, "checkJs": true, "noEmit": true, "strict": true, "types": [] }, "contentMappers": [ { "package": "dhtml-content-mapper", "extensions": [".dhtml"] } ], "include": ["src"]}include selects the source directory, and the registration tells TypeScript which package handles .dhtml. JavaScript checking is enabled because this sample returns JavaScript for ordinary HTML scripts and event handlers. The source deliberately contains two invalid expressions.
<script> const count = 1; count.toUpperCase();</script><button onclick="true && missing()">Check</button>count is a number, and missing has no declaration. The event handler also contains two character references that the mapper must decode before checking. Running ./node_modules/.bin/tsc --runExternalCode --pretty false produces these diagnostics and exits with status 2.
src/page.dhtml(3,9): error TS2339: Property 'toUpperCase' does not exist on type '1'.src/page.dhtml(1,15): error TS7006: Parameter 'event' implicitly has an 'any' type. This location is in virtual code produced by the content mapper 'dhtml-content-mapper@0.1.0' and has no corresponding location in the original file.src/page.dhtml(5,34): error TS2304: Cannot find name 'missing'.The first diagnostic points into the original script. The last points at missing in the original attribute, after the character references. The middle diagnostic comes from the sample’s generated event-handler function. With strict enabled, its untyped event parameter is an error.
Although printed against src/page.dhtml, (1,15) means virtual line 1, column 15, where event starts in the generated handler shown below. The original HTML’s first line is only <script>.
The generated event error shows why a correct source map cannot fix incorrect or insufficiently typed generated code. It can only tell TypeScript which generated ranges correspond to source ranges, and leave the rest identified as virtual code.
A diagnostic range isn’t an editable range
The sample’s transform implementation generates the following supplemental output for the event handler. This is the returned text, inspected by calling transformHtml with UTF-16 positions.
void function(event){true && missing()}The wrapper adds a function scope and a parameter absent from the HTML. Each & occupies five source characters but becomes one & in the output. Together, the two references shrink ten source characters to two virtual characters, an eight-character difference.
One offset per file cannot describe this transformation. The protocol’s span map records correspondence for individual ranges. It also negotiates UTF-8 or UTF-16 positions, so a mapper must use the agreed units rather than assume one position unit per character.
The span-map documentation distinguishes exact, length-preserving Verbatim spans from non-verbatim correspondences. A diagnostic can point at a transformed range without making that range safe to rewrite. TypeScript permits edits back into the original only through exact, length-preserving Verbatim mappings.
In this run, the returned map marks each decoded & as an Atom span and missing() as part of a Verbatim span. An edit inside the Verbatim span, such as renaming missing once it is declared, could map back to the HTML. Edits cannot map back through either generated &. This follows from the returned mappings and the documented edit rule, rather than a separate editor test.
Editor requests use the mappings in reverse. A cursor position maps into virtual code, TypeScript performs the operation, and visible results map back. A mapper can select which spans participate in each operation. This requires more work than placing an error underline. The implementation also allows framework-specific language servers alongside TypeScript’s baseline support.
Enabling a mapper runs its code
The sample’s package manifest declares node ./dist/mapper.js as its executable command. The protocol permits any command, so mappers need not be JavaScript programs. Resolving a package identifies the program to run, not a sandbox around it.
Without --runExternalCode, the same beta run exits with status 2. The trimmed output includes the explicit permission error:
tsconfig.json(9,3): error TS18068: Content mappers require the '--runExternalCode' command line flag to be enabled.The flag authorizes the execution needed for checking. It does not certify the mapper’s code or dependencies. In CI, treat it like enabling another build tool: pin and review the dependency, and run it without deployment credentials or other unnecessary secrets. A lockfile pins what runs. It does not establish that the code is safe.
According to the beta announcement, VS Code requires both workspace trust and content-mapper permissions. The implementation write-up documents the trust check. VS Code passes --runExternalCode only in trusted workspaces. Otherwise, the language server ignores contentMappers. The announcement also documents js/ts.contentMappers.enabled as a way to disable the integration.
Trusting a repository and granting a mapper permission are separate decisions. Grant neither for a repository you haven’t reviewed.
Adopt a mapper before writing one
For a framework project, start with the framework’s maintained integration and verify the exact version against your TypeScript version. Test diagnostics in templates as well as scripts, then test rename and quick fixes separately. The sample’s generated event error shows that running a mapper does not establish the quality of its checks.
Writing a mapper makes sense when you own a file format and can define its translation precisely. Its tests need to cover malformed source, generated helpers, character-reference decoding and position encodings. Also test whether edits change the intended original range.
Keep the framework’s own compiler in the build. The implementation write-up documents that content-mapped files do not emit JavaScript, although declaration emit is supported. A mapper translates a file for type analysis. It does not replace the framework’s rendering or bundling pipeline.
The beta’s ambient module declarations with import attributes, such as declare module "*" with { type: "css" }, describe import types rather than translate embedded expressions for checking.
The iteration plan’s table predates the September 1 release-slip update. That update proposed delaying the beta about two weeks and left revised release-candidate and final dates to come.
The beta shipped October 20. Its announcement gives the later release-candidate target as “in about a month from now”, roughly November 20, with final release “shortly thereafter”. Neither is a fixed release date.
For adoption, require both useful errors and safe editor edits on representative files. If those checks pass, a maintained mapper can bring embedded code into the same checking path as ordinary TypeScript. If they don’t, keep the existing framework checker until the mapper handles the missing cases.
Reproduce the beta run
The commands below build the pinned sample and link it into an isolated test project without changing an existing application’s dependencies. They require Git, npm and Node.js. The recorded run used Node.js 26.12.0 on Linux. Run them in a new directory.
git clone https://github.com/DanielRosenwasser/content-mapper-sample.git samplegit -C sample checkout 2608e487731056ecd981a48ddcb286991694c8bfnpm install --prefix check --ignore-scripts --no-audit --no-fund --save-exact \ typescript@7.1.0-beta @types/node@26.2.0ln -s "$(pwd)/check/node_modules" sample/node_modulescheck/node_modules/.bin/tsc -p sample/dhtml-content-mapper/tsconfig.build.jsonln -s "$(pwd)/sample/dhtml-content-mapper" check/node_modules/dhtml-content-mappermkdir -p check/srcSave the configuration above as check/tsconfig.json and the HTML as check/src/page.dhtml. The local link uses the sample’s private package directly, rather than assuming a published package exists. From check, run these commands individually to compare the permission error with the mapped diagnostics.
The last check and the inspection command below both run the sample’s code. Review it first.
./node_modules/.bin/tsc --version./node_modules/.bin/tsc --pretty false./node_modules/.bin/tsc --runExternalCode --pretty falseThe version command prints Version 7.1.0-beta. Each check exits with status 2 because the first lacks permission and the second checks deliberately invalid code. The no-flag output also includes TS18003, “No inputs were found”, which the excerpt above trims.
To inspect the generated handler, call the sample’s transform directly from check with the same source file.
node --input-type=module -e 'import { readFileSync } from "node:fs";import { transformHtml } from "../sample/dhtml-content-mapper/src/html.ts";const result = transformHtml(readFileSync("src/page.dhtml", "utf8"), "utf-16");console.log(result.supplemental[1].text);'The second supplemental output contains the generated function shown above. The first contains the script block, while the primary output is empty in this sample.