Skip to content

Consider deprecating namespaces declared with the module keyword #51825

Description

Background

Today, TypeScript has two uses of the module keyword.

The first is called an "ambient module declaration".

declare module "some-library" {
  // ...
}

declare module "*.css";

While we often prefer people author declaration files and submit them to DefinitelyTyped, the declare module syntax here is fine.

The second use is for namespace declarations.

module foo {}
declare module bar.baz {}

This is odd - these are called "namespace" declarations? Well, namespace declarations support using either the namespace keyword, or the module keyword. You could replace module with namespace in that last code snippet and it would work the same.

But why? Why support two ways to do the same thing? Because originally, TypeScript was designed with speculation that JavaScript would have two organizational schemes: "external modules" and "internal modules". External modules were files with isolated top-level scopes that could import/export values. Internal modules were named blocks of code that could export values.

Eventually, JavaScript just got "external modules" and called them "modules". TypeScript was left with two concepts and eventually it was too confusing. So in TypeScript 1.5, we introduced the namespace keyword to make the distinction clearer. Over time, the community preferred using the namespace keyword and linters discouraged the use of module in their place.

Recently, TC39 has been discussing two additions to JavaScript - module expressions and module declarations. These proposals have some overlap with namespaces, but ultimately act different and enable different functionality. Regardless of whether we support the proposals or they go in, during discussion, most people were surprised that the use of module for namespaces wasn't at least deprecated.

As we discuss TypeScript 5.0, we're considering some cleanup in the flag and language space.

Deprecation Proposal

In #51813, we discussed the matter and felt comfortable with deprecating the use of the module keyword in namespace declarations. Deprecation plans are guided by plans discussed at #51000.

What this would amount to is a graceful parse on any namespace declaration declared with module. In editor scenarios, our language service would provide a quick fix to replace that keyword with namespace. When "ignoreDeprecations"": "5.0" is turned on, we would still provide an editor suggestion which would enable the same quick fix.

Caveats

This plan seems sound in implementation files; however, if a module-declared namespace appears in a declaration file from a 3rd party package, you're out of luck. At best, we could only error on implementation files.

So it's unclear if we'll be stuck with module keyword-declared namespaces in .d.ts files forever, or if this is possibly the first step in phasing that out too. Even if we wanted to support the module declarations proposal, it seems unclear whether we could when declaration files still contain module keyword-declared namespaces.

Reconsideration

Here's a few points to consider:

  • The work to deprecate is higher than the maintenance of the module keyword over the last 8 years. In fact, writing this deprecation plan was more work than that.
  • Forcing namespace users off of module in implementation files seems fine, but it feels kind of pointless if we can't break them in declaration files.
  • To some extent, erroring on a keyword that no new code uses anyway feels like a break for the sake of purity. It heavily favors conceptual cleanliness for implementers without much benefit for people using TypeScript.

Activity

  1. fatcerberus commented on Dec 8, 2022

    @fatcerberus

    To some extent, erroring on a keyword that no new code uses anyway feels like a break for the sake of purity. It heavily favors conceptual cleanliness for implementers without much benefit for people using TypeScript.

    If module as an alias for namespace isn’t deprecated, won’t it eventually clash with the mentioned ES proposal(s)? So it seems like maintaining the status quo simply isn’t an option in the long term, and the points made under “Reconsideration” are therefore moot. See: the turmoil caused by experimentalDecorators (which is flagged!)

  2. robpalme commented on Dec 29, 2022

    @robpalme

    Thank you for raising this!

    This looks like an excellent deprecation opportunity that will reduce the current confusion caused by having two ways to declare namespaces. Especially when it's clear cut that one keyword is preferred (namespace) and the other keyword is legacy (module). It's great that TypeScript can evolve and steer users towards intended usage.

    Experience in Bloomberg

    Here is some context on general namespace usage within the Bloomberg JS environment. This runtime approximates a pure ES modules-based environment which means no need to cater to CommonJS.

    Linting in Bloomberg’s environment ensures that namespaces are only permitted in declaration files and can only use the modern declare namespace syntax. Meaning namespaces are not used in any files that generate JavaScript. This ensures that non-JS syntax is only used for static type annotations that disappear at build-time.

    When one imposes linting restrictions on a developer community, it usually results in questions like "why am I not allowed to do that?" The internal feedback on these restrictions we've heard is:

    • Zero users have asked for the legacy module syntax for namespaces (in either declarations or *.ts files)
    • One or two users have asked to use namespaces in *.ts files
      • ...and after explaining to them that it wasn't essential or recommended, they dropped the request
      • Relatively speaking, probably 20x more people ask why enum is unavailable in *.ts files compared to why namespaces are not available
    • Usage of modern declare namespace syntax in declaration files is used a small amount (3% of projects)
      • Lots of these are where folk are pairing a hand-authored declaration file alongside a yet-to-be-converted-to-TypeScript *.js file. A quick inspection suggests these could all be better if they were migrated to regular interfaces.
      • Many of these namespace usages are for vendored code from npm/DefinitelyTyped where the folk doing the vendoring have tried to avoid forking/modifying the original declaration files.

    The General Case for Removal

    (Let's ignore the specifics of what module might be used for in future)

    The main argument I would make here is that it's positive for the TypeScript ecosystem as a whole to minimize confusing relics. And it's positive for the JavaScript ecosystem to gain access to prime ergonomic syntax.

    Given the modern-day lack of enthusiasm for any kind of namespace usage, module-keyword namespaces are a niche of a niche. Whilst they may have a meaningful count on public code searches, they are not widely understood. Even experienced TypeScript developers struggle to recall and explain the difference between ambient module declarations and module-keyword namespaces. Fixing this confusion via a clear and decisive deprecate-then-remove story is a net win that justifies the migration cost in my opinion.

    With any migration, it's all made easier by starting earlier, which means starting as soon as possible on the implementation files. With the current deprecation proposal, that will be around three years until full removal from implementation files. That sets the scene for the subsequent removal from declaration files once usage is low enough.

    Next Step Suggestions

    Auto-Modernizing Declaration Emit

    Today compiling the TypeScript file declare module foo {} results in the DTS file declare module foo {} rather than the preferred DTS file declare namespace foo {}

    To accelerate migration within the declaration file ecosystem, maybe we could immediately update TypeScript's declaration emitter to output modern syntax even if the implementation file continues to use the legacy syntax.

    DefinitelyTyped Migrations

    This could be a big topic all by itself. Given that there is some level of ecosystem centralization around DefinitelyTyped, can we seed trends there to assist declaration syntax migrations?

    • Can we enhance the DefinitelyTyped linting to warn/reject on deprecated syntax in declaration files?
    • Can we automatically generate PRs to the source repos that contribute to DefinitelyTyped?

    I'd like to hear more from DT experts on this.

    Telemetry

    Another big topic. VS Code already implements opt-out remote reporting of TypeScript language server crash stacks today. The documentation says it also passes on some usage data. Maybe this could be expanded, in a very minimal anonymized way, to convey when usage of a legacy TypeScript feature has been detected. This would help provide confidence that the final removal from declaration files could proceed without impacting too many users.

    Summary

    Deprecating and removing the legacy keyword usage seems like an excellent move and is highly appreciated. Whilst the keyword could be left around forever as a curiosity, taking proactive steps towards improving both the TypeScript and JavaScript languages helps all users and is the kind of healthy stewardship TypeScript is known for.

  3. robpalme commented on Jan 31, 2023

    @robpalme

    Chi Leung (@CC972) is about to start work on Auto-Modernizing Declaration Emit as described above supported by Titian Cernicova-Dragomir (@dragomirtitian).

  4. JoshuaKGoldberg commented on Jul 18, 2023

    @JoshuaKGoldberg
    Contributor

    DefinitelyTyped Migrations

    I'm not a DT expert but have been working with DT folks recently on DefinitelyTyped/DefinitelyTyped#65993. The points you've suggested all sound very doable to me.

    Can we enhance the DefinitelyTyped linting to warn/reject on deprecated syntax in declaration files?

    Yes: https://typescript-eslint.io/rules/prefer-namespace-keyword. The repo already has support for typescript-eslint rules. We can enable that rule in the repo and use the rule's auto-fixer.

    Can we automatically generate PRs to the source repos that contribute to DefinitelyTyped?

    Yes, and I'd be happy to help with that. One can imagine a script like:

    for repo in a b c; do
      gh repo clone a
      cd a
      npx eslint . --config /path/to/just/prefer-namespace-keyword/config --fix
      # git commands to create branch and send PR
      cd ..
    end
  5. robpalme commented on Jul 19, 2023

    @robpalme

    My goodness. Josh Ghoulberg 👻 (@JoshuaKGoldberg) I reviewed your linked DT issue and you are not messing around.

    This is an excellent plan. Thank you for volunteering.

    If you need anything, please shout and I will arrange assistance.

  6. JoshuaKGoldberg commented on Aug 5, 2023

    @JoshuaKGoldberg
    Contributor

    I started on DefinitelyTyped/DefinitelyTyped#66292. It was actually pretty quick. If you want me to apply the same fixing to any other repo, I can be shouted at here/anywhere and/or tagged an issue. (Rob Palmer (@robpalme) I do love the idea of giving you an excuse to tell me to go do work 😄)

    The work to deprecate is higher than the maintenance of the module keyword over the last 8 years. In fact, writing this deprecation plan was more work than that.

    While the maintenance cost may be near-zero, the education cost is likely higher. I've found that I've had to repeatedly explain to learners why searches for "typescript modules" & similar terms end up with this old module keyword. It's a mild pain that sometimes trips people up. +1 to removing on those grounds.

  7. robpalme commented on Aug 5, 2023

    @robpalme

    You are a good egg, Josh. Thanks for pushing this forwards.

  8. robpalme commented on Mar 1, 2024

    @robpalme

    As a refresher on the current status:

  9. robpalme commented on Mar 22, 2024

    @robpalme

    After speaking with Ryan Cavanaugh (@RyanCavanaugh) it seems like there may still be some concern about impacting active projects that want to upgrade TypeScript but also consume legacy DTS files. For example, where they vendored/snapshotted DTS files containing the old keyword from an unmaintained project.

    So what can we do now to keep things moving forwards?

    JSDoc offers the /**@deprecated*/ tag that does not error, but provides visual feedback of the deprecation along with potential for a quickfix.

    image

    How about we do the same for the module keyword? This would only be at the declaration site and only on the keyword, not the identifier.

    module

    A built-in quickfix would be offered to convert this to namespace. This would provide a useful nudge forwards to the modern keyword without breaking compatibility.

  10. DanielRosenwasser commented on Mar 22, 2024

    @DanielRosenwasser
    MemberAuthor

    I think we'd be okay with something that issues a language service deprecation diagnostic. I don't know if we really need the quick fix, but not opposed either.

  11. RyanCavanaugh commented on Mar 22, 2024

    @RyanCavanaugh
    Member
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

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions