Skip to content

Generate a single declaration file for external modules #2568

Closed

Description

If compiling external modules, with --declaration, each module gets its own declaration file. this is not useful for library authors, who probably want to hand off a single .d.ts file for all their modules. the only way to do this now, is to either hand edit the file, or use a tool to concatenate the output.

This becomes even more desirable with ES6 module export/import where a module can reexport exports of another module.

See discussion in #2516 (comment) for more details.

Activity

  1. mhegazy commented on Mar 31, 2015

    @mhegazy
    ContributorAuthor

    Also related to #17

  2. basarat commented on Mar 31, 2015

    @basarat
    Contributor
  3. NoelAbrahams commented on Apr 1, 2015

    @NoelAbrahams

    I would like to have a generate-single-declaration-file option (without combining JavaScript output) for _internal modules_ as well. See #192.

  4. ToddThomson commented on Apr 2, 2015

    @ToddThomson
    Contributor

    My experiences with Typescript 1.5 have been positive. The only thing that keeps giving me grief is the internal "module" ( which is only a namespace ). I really don't like fighting back and forth between internal and external/real/es6 modules and what I am allowed to do with both ( mostly the grief that comes from defining external module bundles ). I really only want to use the ES6 module syntax going forward.
    I have no issues with 1 file = 1 es6 module and optimizing during a production build. However there are times that I want to bundle files/modules together at compilation time. Even if this just produced a named typescript definition file I would be happy.
    I am wondering if you can leverage the tsconfig.json "project" file to provide "bundles". A bundle would look like:
    "bundles": {
    "bundle-name": [ "some/module", "another/module" ],
    "another-bundle-name": [ "yet-another-module" ]
    }
    An appropriate arg could be used for command line builds.

  5. ToddThomson commented on Apr 17, 2015

    @ToddThomson
    Contributor

    I've put together an npm module ( TsProject ) that uses the tsconfig.json file to define bundles. It generates a typescript source file based on the dependencies in a source file as specified by a bundle. The generated typescript source file is output along with the compiled javascript and definition file.

  6. poelstra commented on Apr 19, 2015

    @poelstra

    I'm not too sure it's really "a single .d.ts file" that we'd need.

    Having one .d.ts file per .emitted .js file is a very good fit for the CommonJS way of working.
    Relevant advantages I see:

    • possibility for having multiple versions of the same module
    • only rewriting changed files, without having to rewrite the complete bundle

    Note that with external modules, the name of an external module is typically not present in the file itself. It is determined by its filename. So, I'd vote for keeping the currently generated .d.ts files as-is, and not e.g. put 'declare module "my-module" {}' inside of them, not even in an 'overall bundle file'.

    Tools like dts-bundle and dts-generator seem to prefer to write out one file, but I believe this is mainly because TypeScript doesn't allow specifying relative paths for the import()'s in .d.ts files.

    I think that if TSC would allow those paths even in .d.ts files, dts-bundle or dts-generator would no longer be necessary.

    Still, a solution needs to be found for any other 'really external' modules (i.e. different npm packages, not just different .ts files inside the package) that are referenced, as this would basically mean that the /// <reference ...> line of the package would need to be 'local' to that package's .d.ts file. Otherwise these really external modules will start conflicting with similar modules in other packages.

    TSC disallows having an external module be declared multiple times, which you can only get to work if all references are pointing to the same definition file. So, let's not try to 'throw away' that information by bundling, but support the case of leaving the files where they are, and use the 'real' filesystem to resolve the references.

    Note: my use-case is mainly 'plain' CommonJS, using TSC's --outDir option, and using browserify for the browser. I guess that the single-.d.ts-file case would be handy for TSC's --out option.

  7. basarat commented on Apr 20, 2015

    @basarat
    Contributor

    Worth sharing here : #2829 all it needs is the path to your package.json + tsconfig.json to generate and consume node packages. no additional configuration required.

  8. basarat commented on Apr 20, 2015

    @basarat
    Contributor

    Martin Poelstra (@poelstra)

    my use-case is mainly 'plain' CommonJS, using TSC's --outDir option, and using browserify for the browser. I guess that the single-.d.ts-file case would be handy for TSC's --out option

    Not true. Look at : https://github.com/TypeStrong/atom-typescript-examples/blob/master/node/lib/main.ts You can import any file you have exported despite using a single .d.ts definition file

    Again the details of the proposal : #2829

  9. poelstra commented on Apr 20, 2015

    @poelstra

    Basarat Ali Syed (@basarat) I know about that possibility, yes, and in fact, that's what we use today (using output of dts-bundle at work, and recently started using dts-generator at home).

    The problem I see with it though, is that it will break as soon as more than one dependency level is introduced.

    Consider this dependency tree:

    • myprogram
      • mylib
        • myutils@1.0
      • myotherlib
        • myutils@2.0

    In the case of generating .d.ts files with declare module "myutils" { ... }, etc, all of these declarations will be in the same 'global module name namespace'. Thus, mylib and myotherlib cannot each use their own version of myutils.

    This is not a problem as long as myutils is only used internally (and thus its types are never exposed through mylib/myotherlib), but that will often not be the case.

  10. basarat commented on Apr 20, 2015

    @basarat
    Contributor

    Martin Poelstra (@poelstra) awesome. Perhaps you can hash out what a .d.ts would look like for mylib/myotherlib/myutils@1.0/ myutils@2.0

    Then I can have something to work from and understand your proposal better (perhaps then I can even implement it).

  11. 20 remaining items

  12. removed
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    on May 4, 2016
  13. bennycode commented on May 31, 2017

    @bennycode
    Contributor

    Mohamed Hegazy (@mhegazy), how can declaration output files be bundled?

    I followed up on #5090 and there it's mentioned that you need to set --module along with --outFile. Unfortunately this doesn't work with "module": "commonjs" because of:

    error TS6082: Only 'amd' and 'system' modules are supported alongside --outFile.

  14. mhegazy commented on May 31, 2017

    @mhegazy
    ContributorAuthor

    why do you want to bundle your declarations if you are using node? the .js files are not bundled either.

  15. delagen commented on May 31, 2017

    @delagen

    Mohamed Hegazy (@mhegazy) if TS itself cannot bundle JS files, that does not mean that no any other tool.
    JS files can be bundled with rollup, webpack or any other bundler.
    For now I bundle declarations using typings tool, that works, but result all declarations in file, not only exported by main entry. Result declaration bundle contains from many declare module sections with add main with package.json module name. But this is more convenient for me instead of place one compiled module (sometimes with sourcemap file) in package uploaded to registry with many d.ts files generated by TS compile.

  16. bennycode commented on Jun 15, 2017

    @bennycode
    Contributor

    Mohamed Hegazy (@mhegazy) I am getting lost. Maybe you can help me.

    I have a TypeScript application with the following configuration (tsconfig.json):

    {
      "compilerOptions": {
        "declaration": true,
        "lib": ["DOM", "ES6"],
        "module": "CommonJS",
        "moduleResolution": "node",
        "noEmitOnError": true,
        "noImplicitAny": true,
        "noImplicitReturns": true,
        "outDir": "dist/commonjs",
        "removeComments": true,
        "rootDir": "src/ts",
        "sourceMap": false,
        "target": "ES5"
      },
      "exclude": [
        "dist/commonjs",
        "node_modules"
      ]
    }

    When I run tsc, then the TypeScript compiler creates several JavaScript files (*.js) and several TypeScript definition files (.d.ts):

    Screenshot

    Now I want to distribute my code as npm package and I would like to support types for people using my code but I don't know where I should point the types property in my package.json to.

    The documentation says: "Set the types property to point to your bundled declaration file" but I don't have a definition bundle (like main.d.ts or index.d.ts). Instead I have several *.d.ts files. 😕

    So what can I do now to publish my npm package along with it's definitions?

  17. delagen commented on Jun 15, 2017

    @delagen

    bennyn You can publish it as is. (all .js and .d.ts files)

  18. orefalo commented on Jun 15, 2017

    @orefalo

    bennyn I have the same issue
    Sergey Vohmyanin (@delagen) How? file: [ "dist/" ] ?

  19. ikatyang commented on Jun 15, 2017

    @ikatyang
    Contributor

    bennyn Olivier Refalo (@orefalo)

    For short, types is the main for types.


    Just like the main field in package.json, you have to specify the declaration file for the main file, then TS will follow the module resolution (import/export) to find those related type.

    For example:

    (package.json)

    {
      "main": "lib/index.js",
      "types": "lib/index.d.ts"
    }
  20. delagen commented on Jun 15, 2017

    @delagen

    Olivier Refalo (@orefalo) if file d.ts place is near the main js the types section is unneded. TS compiler try to load declaration with the JS file loaded, so no action needed.
    If declaration file is different named than main file, so it must be specified in types property of package.json.

  21. bennycode commented on Jun 16, 2017

    @bennycode
    Contributor

    Ika (@ikatyang) I have done what you have suggested. In my library I pointed types to my index.d.ts file:

    package.json

    "types": "./dist/commonjs/index.d.ts"

    ./dist/commonjs/index.d.ts

    import WireAPIClient from './core/WireAPIClient';
    export = WireAPIClient;

    Afterwards I tried to use my library in another TypeScript project:

    import {WireAPIClient} from '@wireapp/api-client';

    This caused the following error:

    error TS2497: Module '"D:/dev/projects/bennyn/wire-web-core/node_modules/@wireapp/api-client/dist/commonjs/index"' resolves to a non-module entity and cannot be imported using this construct.

    So I changed my import statement:

    import WireAPIClient = require('@wireapp/api-client');

    Which then led to the following errors:

    node_modules/@wireapp/api-client/dist/commonjs/auth/AuthAPI.d.ts(15,30): error TS2304: Cannot find name 'LoginData'.
    node_modules/@wireapp/api-client/dist/commonjs/auth/AuthAPI.d.ts(16,22): error TS2304: Cannot find name 'LoginData'.
    node_modules/@wireapp/api-client/dist/commonjs/auth/AuthAPI.d.ts(16,42): error TS2304: Cannot find name 'AccessTokenData'.
    node_modules/@wireapp/api-client/dist/commonjs/auth/AuthAPI.d.ts(18,27): error TS2304: Cannot find name 'AccessTokenData'.
    

    LoginData and AccessTokenData are classes from my library and they are in the same directory as AuthAPI, that's why there is no import statement in AuthAPI for them. So it seems like the TS module resolution doesn't work properly in my case?

  22. ikatyang commented on Jun 17, 2017

    @ikatyang
    Contributor

    bennyn I have no idea what's going on to your compiled dts file, since I cloned your PR and execute yarn run dist, I found that your compiled AuthAPI.d.ts somehow miss the triple-slash directives.

    (AuthAPI.d.ts)

    +/// <reference path="LoginData.d.ts" />
    +/// <reference path="AccessTokenData.d.ts" />

    It compiled fine for me (with triple-slash directives exist), and thus the following import works fine.

    import WireAPIClient = require('@wireapp/api-client');

    NOTE: There are several unlinked definitions in your code (does not use import or triple-slash-directives, so TS cannot find them), you have to correct them to get the result.

  23. bennycode commented on Jun 18, 2017

    @bennycode
    Contributor

    Ika (@ikatyang) Thank you so much for your support!

    I forgot to regenerate the .d.ts files in the @wireapp/api-client project. After I did that everything was running smooth. I've also found a better alternative to the triple-slash directives:

    Before (no interface export + triple-slash)

    LoginData.ts

    interface LoginData {
      email: string;
      password: number | string;
    }

    AuthAPI.ts

    /// <reference path="LoginData.d.ts" />
    import {AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios';
    
    export default class AuthAPI {
      constructor(private client: HttpClient) {}
    
      static get URL() {
        return {
          COOKIES: '/cookies',
          INVITATIONS: '/invitations'
        };
      }
    
      public postCookiesRemove(login: LoginData, labels?: string[]): AxiosPromise {
        const config: AxiosRequestConfig = {
          data: {
            labels: labels,
            password: login.password.toString(),
          },
          method: 'post',
          url: `${AuthAPI.URL.COOKIES}/remove`,
        };
    
        return this.client.sendRequest(config);
      }
    
      // More code follows here...
    }

    After (interface export + interface import)

    LoginData.ts

    interface LoginData {
      email: string;
      password: number | string;
    }
    
    export default LoginData;

    AuthAPI.ts

    import LoginData from './LoginData';
    import {AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios';
    
    export default class AuthAPI {
      constructor(private client: HttpClient) {}
    
      static get URL() {
        return {
          COOKIES: '/cookies',
          INVITATIONS: '/invitations'
        };
      }
    
      public postCookiesRemove(login: LoginData, labels?: string[]): AxiosPromise {
        const config: AxiosRequestConfig = {
          data: {
            labels: labels,
            password: login.password.toString(),
          },
          method: 'post',
          url: `${AuthAPI.URL.COOKIES}/remove`,
        };
    
        return this.client.sendRequest(config);
      }
    
      // More code follows here...
    }

    Outcome

    With export default LoginData; I can now use the @wireapp/api-client library like this:

    import Context from "@wireapp/api-client/dist/commonjs/core/Context";
    import LoginData from "@wireapp/api-client/dist/commonjs/auth/LoginData";
    import WireAPIClient = require('@wireapp/api-client');
    
    const client: WireAPIClient = new WireAPIClient({
      rest: 'https://prod-nginz-https.wire.com',
      ws: 'wss://prod-nginz-ssl.wire.com'
    });
    
    const credentials: LoginData = {
      email: 'name@mail.com',
      password: 'secret',
      persist: false
    };
    
    client.login(credentials).then((context: Context) => {
      console.log(`Got self user with ID "${context.userID}".`);
    });

    This is exactly what I wanted to do. Thank you very much! 🍻

  24. locked and limited conversation to collaborators on Jun 18, 2018
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

    FixedA PR has been merged for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions