Skip to content

Feature Request: Allow Content Mappers to opt-in to transforming files end in built-in extensionsΒ #65092

Closed as not planned

Description

@GoodbyeNJN

πŸ” Search Terms

content mapper, built-in extension, dynamic LSP registration, duplicate inlay hints

βœ… Viability Checklist

⭐ Suggestion

Content Mappers currently target otherwise unsupported file extensions. It would be useful to let a mapper explicitly opt in to transforming files with built-in TypeScript/JavaScript extensions, such as .ts, while keeping the current restriction as the default.

The feature should also define how mapper applicability is scoped. For example, a mapper could explicitly claim selected .ts files, or be responsible for returning an unchanged, correctly mapped result when a file contains no syntax it handles. Simply registering .ts globally could otherwise cause ordinary TypeScript files to be transformed unexpectedly.

When a built-in extension is enabled, LSP capabilities already provided for JavaScript/TypeScript documents should not be registered again for the same documents. This is important for avoiding duplicate language-feature results, such as inlay hints.

πŸ“ƒ Motivating Example

I’m building a tool that adds Result/Option-style early-return syntax to TypeScript. For example, it could transform:

function example() {
    // If this is Err, return early; if it is Ok, unwrap the value.
    const value = getResult().try;
    return Ok(value);
}

into virtual TypeScript such as:

import __$adapter$__ from "user-defined-adapter";

function example() {
    return __$adapter$__.wrap(function* () {
        const value = yield* __$adapter$__.unwrap(getResult());
        return Ok(value);
    });
}

The tool can generate valid TypeScript and mappings back to the original source, but it cannot use Content Mapper for this workflow because the input file is .ts, a built-in extension.

πŸ’» Use Cases

  • User-defined syntax sugar or macro-like transforms within ordinary .ts files.
  • Result/Option workflows that unwrap values and propagate failures.
  • Other source-level transforms that produce valid virtual TypeScript while retaining useful source mappings for language-service features.

The goal is not to add Result/Option semantics to TypeScript itself. It is to let users opt into their own transformation while keeping ordinary TypeScript behavior unchanged unless explicitly configured.

Activity

  1. changed the title [-]Feature Request: Allow Content Mappers to opt in to transforming built-in TypeScript files[/-] [+]Feature Request: Allow Content Mappers to opt in to transforming TypeScript/JavaScript files[/+] on Nov 1, 2026
  2. changed the title [-]Feature Request: Allow Content Mappers to opt in to transforming TypeScript/JavaScript files[/-] [+]Feature Request: Allow Content Mappers to opt-in to transforming files end in built-in extensions[/+] on Nov 1, 2026
  3. GoodbyeNJN commented on Nov 1, 2026

    @GoodbyeNJN
    Author

    Additional finding from a local experiment

    To test whether my use case could work with .ts files, I temporarily bypassed the built-in-extension checks in tsc/internal/contentmapper/contentmapper.go and packages/vscode-typescript/src/contentMapperContributions.ts. This was only a local workaround to investigate the behavior; I’m not proposing that the checks be removed as the fix.

    With .ts registered as a Content Mapper extension, each inlay hint appeared twice. The TypeScript client already provides inlay hints for TypeScript documents through its static JS/TS document selector, while the Content Mapper registration added a dynamic selector matching **/*.ts. The same document therefore matched both registrations.

    I confirmed the cause by locally filtering built-in JS/TS extensions out of the Content Mapper’s dynamic LSP registrations in tsc/internal/lsp/server.go, leaving those documents to the existing JS/TS registration. With that change, the duplicate hints disappeared, while the Content Mapper still transformed the .ts file.

    	extensions = func() []string {
    		result := make([]string, 0, len(extensions))
    		for _, extension := range extensions {
    			isStaticallyHandled := false
    			for _, supportedExtensions := range tspath.AllSupportedExtensions {
    				for _, supportedExtension := range supportedExtensions {
    					if len(extension) >= len(supportedExtension) &&
    						strings.EqualFold(extension[len(extension)-len(supportedExtension):], supportedExtension) {
    						isStaticallyHandled = true
    						break
    					}
    				}
    				if isStaticallyHandled {
    					break
    				}
    			}
    			if !isStaticallyHandled {
    				result = append(result, extension)
    			}
    		}
    		return result
    	}()
    	if len(extensions) == 0 {
    		return nil
    	}

    This seems like an LSP registration interaction that would need to be handled if Content Mappers are ever allowed to opt in to built-in extensions.

    An opt-in mechanism for built-in extensions could also enable other use cases, such as the one discussed in #64917 and integrations with libraries like Effect-TS.

  4. RyanCavanaugh commented on Nov 2, 2026

    @RyanCavanaugh
    Member

    This is very much an intentional exclusion. Content mappers are not here to provide language variants of TS, and we don't want the meaning of a TS file to be subject to unbounded re-interpretation depending on the tsconfig file. This also isn't composable in any meaningful way. I'd recommend doing something like .ets if you want to make a specific new dialect of TS.

  5. loopingz commented on Nov 2, 2026

    @loopingz

    It was possible in 7.1.0-dev.20260929.1 opened the door to nice additional analysis, like validating additional SQL query based on the known schema. I understand the reasoning from Ryan Cavanaugh (@RyanCavanaugh) but would probably be nice to have the option/decision left to the user in my opinion.

  6. RyanCavanaugh commented on Nov 3, 2026

    @RyanCavanaugh
    Member

    Failing to block this doesn't mean it would just work. Configuration like outputExtensions fails to make any real sense if the input is a .ts file, refactorings that assume they can apply to any .ts file stop working, it's unclear how to deal with multiple CMs all claiming to each handle .ts files (what order do they run in?), and so on.

  7. DetachHead commented on Nov 5, 2026

    @DetachHead
    Contributor

    i understand not wanting to support this specific use case, but being able to import json files as const has been a highly requested feature for years (see #32063 which has over 1300 upvotes) and content mappers seem like the perfect solution, if only we could use them on regular .json files.

    what if the built-in content mapper for .json files was only applied when resolveJsonModule is false? that way, users can choose to override the built in .json content mapper with one that narrows the type.

  8. typescript-automation commented on Nov 8, 2026

    @typescript-automation
    Contributor

    This issue has been marked as "Declined" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions