question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Proposal: Enable declaration files for non-js-extensioned files

See original GitHub issue

Suggestion

Ref #49970 and confusion therein.

⭐ Suggestion

Today, TS will only load declaration files for .js, .cjs, and .mjs files (and TS+jsx equivalents). There is no actual mechanism for specifying type definitions for a relative import for files with another extension - nonrelative imports can be defined using an ambient module declaration (see the widely used declare module "*.css";), or via import or export map entry, while relative imports have no mechanism available.

In traditional cjs resolution, this wasn’t obviously a problem, since if you wrote require("./style.css"), we would fail to load the .css file (it’s not a javascript or typescript extension so we wouldn’t even try), and fall back to cjs directory and extension resolution - meaning we’d eventually look for "./style.css.js" and accompanying "./style.css.d.ts", which was close enough for most people’s purposes. Now, we have a prominent runtime that does not have extension searching (esm in node/some bundlers/browsers), where there’s no mechanism to provide types for the specifier, but in some conditions do support importing non-js extension files.

📃 Motivating Example

import {whatever} from "./stylesheet.css";
import {whatever} from "./mod.wasm";
import {whatever} from "./component.html";
import {whatever} from "./db.json";

💻 Use Cases

Mostly bundlers and potentially browsers for css/html imports, but also node for the potential to type relative imports of .wasm/.json/.node via declaration file, or other extensions as allowed via custom loader.

📃 Proposal

Generally speaking, we want to keep a 1:1 mapping of runtime file extension to declaration file extension, this way we can always do a simple side-by-side lookup for the declarations for a file imported, a relatively simple mapping of output declaration path back to input filepath, and also capture any important format information implied by the original extension in the declaration filename (eg, if .wasm imports end up being importable as uninstantiated modules in the future, and other modules aren’t). As such, any mechanism needs to be fairly unambiguous.

Given that, I’d have to propose that a

filename.ext

maps to a

filename.d.ext.ts

which is a declaration file (we’d have to update our definition of declarations to be .d.ts, .d.cts, .d.mts, and .d.*.ts).

This does a few things:

  1. It allows any ext to have a unique TS equivalent
  2. Unlike a filename.d.ts, it doesn’t also ambiguously map to a filename.js or filename.otherext
  3. Unlike a filename.ext.d.ts, it doesn’t already map to a filename.ext.js
  4. Unlike a filename.d.ext, it retains a well-known .ts final extension, for relatively painless tooling and editor support

There are some considerations:

  • You could already have a filename.d.ext.ts source file today - it would be a TS source file (that emits a filename.d.ext.js and a filename.d.ext.d.ts) and not a declaration file, making this a technically breaking change. Such a filename is so unwieldy as to be unlikely in my view though, so I don’t think this break should be too bad. In some ways, this is an upside, since external tooling will already recognize and parse these files as TS without modification, even if they don’t see them as declarations yet.
  • Unfortunately, unless we specify otherwise, this does imply you should be able to have a filename.d.js.ts, which behaves identically to a filename.d.ts, and we’d have to prioritize one of the two during lookup. Tentatively, I’d say we should just forbid ts and js extension patterns like this, so .d.js.ts, .d.mjs.ts, .d.cjs.ts, .d.jsx.ts, .d.ts.ts, .d.tsx.ts, .d.mts.ts, and .d.cts.ts shouldn’t be looked up - the existing short forms take their place.
  • In a multi-module-format situation like in modern node, it’s also an open question what format these modules should be interpreted as, which at runtime depends on if the loader used injects the module into the cjs require cache or only synthesizes an esm dynamic module. Because of that distinction (and the inability to import an esm-format-only thing from a cjs format thing), it may be important to encode the expected runtime format into the declaration name as well. For that reason, it would be tempting to reuse the mts and cts extensions; unfortunately this introduces some ambiguity, as all of filename.d.ext.ts, filename.d.ext.mts, and filename.d.ext.cts would map to the same original filename.ext, rather than only one canonical one (and looking up the input filepath from an output declaration file is critical for scenarios like project references, which is why unambiguousness is so helpful here). We could just assume these declaration files are formatless, like ambient modules, and provide the same interface to both cjs and esm callers. That might be good enough to get by. Failing that, the only thing I can think of that preserves filename uniqueness would be some kind of in-declaration-file pragma for asserting the format of the containing file, or a compiler option specifying a global mapping of extension to format (though the later doesn’t hold up well with redistributable libraries). But all that may be unnecessary, since, with the exception of potentially .json, these should largely be authored by hand, so protection from format misuse is maybe less important than with JS files writ large.
  • These mappings should be enabled and looked up in all resolution modes - classic, node, node16, nodenext. It may, however, be appropriate to add an error on non-js non-declaration imports than is only suppressed with a compiler option (eg, allowNonJsImports, in the same vein as resolveJsonModule).
  • Alongside this change, we should also be able to enable our long-supported-but-disabled declaration emit for json documents when resolveJsonModule is set, since it would canonicalize filename.d.json.ts as the declaration path for the json (and thus canonicalize loading such a declaration file at higher priority than the original json document in a wildcard include pattern).

Thoughts?

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:20
  • Comments:10 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
weswighamcommented, Nov 30, 2022

@DanielRosenwasser if a .d.ext.ts file is present, it should take priority (especially since it actually contains the type information), and if the author of such a file wants to go to the original file (because the declarations are generated in some way), they can always include a declaration source map that maps back to the non-declaration file in some way.

@idoros A declare module '*.ext' only applies to nonrelative imports, and only if an actual file lookup for the import fails. So the file definitely has priority if it exists in that case.

2reactions
barak007commented, Aug 2, 2022

I think it’s great to support definition files for other file types. I very much agree with the one to one mapping, it’s the only way forward. But I think the proposed solution is too complex.

A solution that replaces the file extension like x.d.ts for x.js is not ideal because it’s a system that cannot support files without extension or requests where the extension is not provided (resolved extension). It’s also difficult and costly to identify the real filename or its type. it’s really only a convention, it’s context dependent, and only if you know to look for it then you’ll find it, and you’ll have to search for it every time. Any integration that searches files by extension will have to be aware to maybe not process files that contain double (or as proposed, triple) extensions.

On top of that, the proposed solution to add another extension will create many more extensions to look for, (such as, .d.css.ts, .d.wasm.ts), and makes the process of finding if a file is definition file or not, more costly (3 extname calls).

My perhaps slightly radical proposal is to use a completely new dedicated extension like .dts or .tsd, for definition files. I think it will make the process of matching them simple and elegant. All the metadata of the module type (cjs, mjs) is in the original filename, and finding a definition file for a real file is just to add the extension and consider rootDirs. The other way around is just one extname call. Proposal Example:

/path/to/file     -> /path/to/file.dts
/path/to/file.ext -> /path/to/file.ext.dts

Output:
/path/to/file.ts -> /path/to/file.js
                    /path/to/file.js.map
                    /path/to/file.js.dts
                    /path/to/file.js.dts.map

/path/to/file.mts -> /path/to/file.mjs
                     /path/to/file.mjs.map
                     /path/to/file.mjs.dts
                     /path/to/file.mjs.dts.map

/path/to/file.cts -> /path/to/file.cjs
                     /path/to/file.cjs.map
                     /path/to/file.cjs.dts
                     /path/to/file.cjs.dts.map
Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found