Search code examples
javascripttypescripttypesjsdoc

How can I import types from a .d.ts file in a .js file with the same name


I’m writing JavaScript code and want type safety with TypeSript via JSDoc.

Because it’s just nicer to write types in TypeScript I wanted to put the type definitions in a .d.ts file right next to my .js file:

// person.d.ts
export type Person = {
  name: string;
}
// person.js

/** @type {import("./person").Person} */
let person;

person = {
  name2: "sdf", // <-- this should error, but does not
};

The issue is, that this does seem to break the TypeScript checker.

If I rename person.d.ts to foo.d.ts and import ./foo instead, it works.

What is strange, is that TypeScript seems to see and understand the type, it just doesn’t properly interpret it:

Screenshot of VSCode, showing that the type Person is imported properly

Am I doing something wrong?

EDIT: here is my tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "allowJs": true,
    "checkJs": true,
    "rootDir": "."
  },
  "include": ["./**/*.js", "./**/*.ts"]
}

Solution

  • What you're experiencing is actually a very obscure tsc "feature".

    Consider the following tsconfig:

    {
      "include": [
        "**/*.js",
        "**/*.ts"
      ],
      "compilerOptions": {
        "rootDir": ".",
        "listFiles": true,
        "module": "commonjs",
        "allowJs": true,
        "checkJs": true,
        "noEmit": true,
        "skipLibCheck": true
      }
    }
    
    $ tree -L 2
    .
    ├── node_modules
    │   ├── reverse-line-reader
    │   └── typescript
    ├── package.json
    ├── src
    │   ├── person.d.ts
    │   └── person.js
    ├── tsconfig.json
    └── yarn.lock
    

    Now run tsc:

    /home/user/source/module/node_modules/typescript/lib/lib.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.es5.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.dom.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.webworker.importscripts.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.scripthost.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.decorators.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.decorators.legacy.d.ts
    /home/user/source/module/src/person.d.ts
    

    You can see only person.d.ts is picked up by the compiler, even though person.js clearly exists. There are also no reported errors.

    Now remove the include section from tsconfig.json:

    $ tsc
    src/person.js:7:5 - error TS2322: Type '{ name2: string; }' is not assignable to type 'Person'.
      Object literal may only specify known properties, but 'name2' does not exist in type 'Person'. Did you mean to write 'name'?
    
    7     name2: "sdf", // <-- this should error, but does not
          ~~~~~~~~~~~~
    
    /home/user/source/module/node_modules/typescript/lib/lib.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.es5.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.dom.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.webworker.importscripts.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.scripthost.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.decorators.d.ts
    /home/user/source/module/node_modules/typescript/lib/lib.decorators.legacy.d.ts
    /home/user/source/module/src/person.d.ts
    /home/user/source/module/src/person.js
    
    Found 1 error in src/person.js:7
    

    Now magically both person.js and person.d.ts are picked up by the compiler. Why?

    It seems that when using include, tsc will for some reason exclude files it thinks may have been emitted (auto-generated) by tsc in a previous run. For example, if you had a person.ts file and ran tsc, it would produce a file named person.js and person.d.ts (with declaration set to true in tsconfig.json).

    The only explanation I have ever seen is this document which is technically deprecated:

    Please note that the compiler does not include files that can be possible outputs; e.g. if the input includes index.ts, then index.d.ts and index.js are excluded. In general, having files that differ only in extension next to each other is not recommended.

    However, it appears to still be true to this day.

    In short, one solution is to simply remove the include section entirely. It is not needed in this case, and usually not unless you have a fairly complex repository.

    Alternatively, as the documentation suggests, you can move your types into a different folder entirely, or all into a monolithic file.