Skip to content

.d.json.ts file (allowArbitraryExtensions) not working correctly when importing from ESM file with node16/nodenext module settingΒ #57229

Open

Description

@noinkling

πŸ”Ž Search Terms

allowArbitraryExtensions, import, JSON, d.json.ts, nodenext, node16, mjs, mts, "type": "module"

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries

⏯ Playground Link

https://codesandbox.io/p/devbox/json-import-bug-frj89p

πŸ’» Code

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "resolveJsonModule": true,
    "allowArbitraryExtensions": true,
    "noImplicitAny": true
  }
}

foo.json

["foo", "bar", "baz"]

foo.d.json.ts

// In this contrived example we're declaring it as a tuple since TS otherwise infers it as string[]
declare const data: ["foo", "bar", "baz"];
export default data;

index.mts, or index.ts with "type": "module" in package.json

import data from "./foo.json" with { type: "json" };

// Apparently not an array, these all error!
const x = data satisfies any[];
data.forEach((item) => console.log(item));
type Item = (typeof data)[number];

// TS thinks it lives on .default
const x2 = data.default satisfies any[];
data.default.forEach((item) => console.log(item));
type Item2 = (typeof data.default)[number];

πŸ™ Actual behavior

The compiler/intellisense thinks the JSON value lives on the .default property, as if the import were a CommonJS/"synthetic" ES module.

πŸ™‚ Expected behavior

It should know that the "base" (actual default) import holds the JSON value, which is the behaviour when there isn't a .d.json.ts file present and most importantly the runtime behaviour.

Additional information about the issue

I tried renaming to *.d.json.mts but then the file simply doesn't get picked up.

The motivation for using this feature in the first place is because I'm importing a big JSON array of objects that causes a long delay every time autocompletion is invoked or something is hovered and generally makes things unbearably sluggish, and fails to infer the type anyway. Some people reporting the same thing in this issue: #48364

Side note: if you're messing around in the CodeSandbox sometimes the intellisense won't update unless you do something like modify the import path to trigger it. Also if you use the terminal you should use npx tsc because the globally installed version (at the time of writing) is 5.2 which complains about import attribute syntax. So I recommend testing locally.

Activity

  1. RyanCavanaugh commented on Jan 30, 2024

    @RyanCavanaugh
    Member

    I tried renaming to *.d.json.mts but then the file simply doesn't get picked up.

    This worked for me -- the import got treated correctly, the file appears in --listFiles, and errors appear if I introduce an error into it. Can you verify?

    Anyway I think the desired behavior here is that for a .d.whatever.ts file, we treat it as CJS/ESM depending on the package.json's type field

  2. noinkling commented on Jan 31, 2024

    @noinkling
    Author

    This worked for me -- the import got treated correctly, the file appears in --listFiles, and errors appear if I introduce an error into it. Can you verify?

    Ryan Cavanaugh (@RyanCavanaugh) Can't replicate (just tested on 5.4.0-beta). If the json file is present then intellisense just uses the inferred type from that, and when compiling the .d.json.mts appears to be treated as a standard TS file rather than a declaration file, e.g: it outputs a "foo.d.json.mjs" file, and with the --declaration flag also "foo.d.json.d.mts" (notice two "d"s). It'll still appear in --listFiles and can cause compilation errors in a typical setup, so my hunch is that's what you're seeing. Also if I delete the json file (keeping the declaration file) then I get error TS2307: Cannot find module './foo.json' or its corresponding type declarations.

    Compare that to when it's named .d.json.ts: there's no output for it when compiling, and if I delete the json file there's no error (because the declaration file is being picked up).

  3. noinkling commented on Feb 3, 2024

    @noinkling
    Author

    Made an interesting discovery. If I disable resolveJsonModule and allowArbitraryExtensions, and instead create an ambient module declaration (in a standard declaration file) like so:

    declare module '*/foo.json' {
      const data: ["foo", "bar", "baz"];
      export default data;
    }

    I get the same issue with the value only being recognized on .default. Naming the referenced declaration file *.d.ts, *.d.mts or *.d.cts doesn't make a difference, neither does changing the type field in package.json.

    So apparently the issue isn't limited to the allowArbitraryExtensions/resolveJsonModule features, which implies it's something integral to the nodenext/node16 resolution mode. Setting module to esnext (with any of the other resolution modes) makes it behave correctly, just to make it clear.

  4. weswigham commented on Aug 13, 2024

    @weswigham
    Member

    Alright, so for the sake of argument, I have given this a try over here, and instantly I got... very confusing test results and inscrutable behaviors due to the interplay of various formats and interop behaviors. I assumed some changes in behavior, and even those assumptions weren't quite right.

    Anyway I think the desired behavior here is that for a .d.whatever.ts file, we treat it as CJS/ESM depending on the package.json's type field

    After looking at it, I don't think this is a great idea - in fact, I've bamboozled you in the above statement, it's not a new behavior for us to adopt, it's our current behavior - the above change is a noop - a .d.*.ts file has .ts extension, so already falls into the package-json-dependent-format codepath. I think .d.*.ts files should really be forced to cjs format. That way the right way to write the declaration file for

    ["foo", "bar", "baz"]

    is always just

    declare const data: ["foo", "bar", "baz"];
    export = data;

    regardless of context - this gives the likely intended behavior for both esm and cjs consumers (the whole export='d object is available as the default import in esm, or the result of require in cjs). And that you can find here.

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

Metadata

Metadata

Labels

Breaking ChangeWould introduce errors in existing codeBugA bug in TypeScriptDomain: Node ESMLike ES Modules, but specific to Node.js support (cts, cts, mjs, mts)RescheduledThis issue was previously scheduled to an earlier milestone

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions