General TypeScript usage for modules / namespaces

3.7k views Asked by At

I have a standard web page (not an app rendered with angular, react, vue, etc) that uses jQuery, and other libraries.

I want to integrate good practices with TypeScript. What is the best way to do this?

My current idea is to have 3 files:

  1. the index.d.ts (describes the types of my module)
  2. the test.ts (the implementation of the types described in the index.d.ts)
  3. the page.js (the file that uses the javascript defined in the test.js -- output from the test.ts)

I currently have these contents:

index.d.ts

// Type definitions for test 1.0.0
// Project: Test
// Definitions by: Author Name

/// <reference path="../../lib/@types/jquery/index.d.ts" />
declare namespace TestNS {
    export class TestClass {
        // Fields
        element: JQuery;
        value: string;

        constructor(element: JQuery, val: string);

        OnCreate();

        static AttachToJQuery(jq: JQueryStatic): void;
    }

    interface JQuery {
        TestClass(element: JQuery, val?: string): TestNS.TestClass;
    }

    interface JQueryStatic {
        TestClass(element: JQuery, val?: string): TestNS.TestClass;
    }
}

Test.ts (loaded 2nd, after jQuery)

/// <reference path="../../lib/@types/jquery/index.d.ts" />
/// <reference path="./index.d.ts" />

export namespace TestNS {
    export class TestClass {
        // Fields
        element: JQuery;
        value: string;

        constructor(element: JQuery, val: string) {
            this.element = element;
            this.value = val;
            this.OnCreate();
        }

        OnCreate() {
            this.element.data('test-value', this.value);
        }

        static AttachToJQuery(jq: JQueryStatic) {
            //no jQuery around
            if (!jq) {
                return false;
            }

            jq.fn.extend({
                TestNS: function(newVal) {
                    return this.each(function() {
                        var obj = jq(this);
                        new TestNS.TestClass(obj, newVal);

                    });
                }
            });
        }//attach to jquery method (to easily work with noconflict mode)
    }//test class
}

page.js (loaded last)

let newJquery:JQueryStatic = jQuery.noConflict();
TestNS.TestClass.AttachToJQuery(newJquery);

let testElems = newJquery(".testClass");
testElems.TestClass();

My goal is to have my code neatly organized into a namespace in typescript, as well as on the page (but doing so in typescript gives errors related to duplicate identifiers) as well as being modular and extensible. I understand that I should publish my types under the "node_modules/@types" directory, but I simply want them all encapsulated in this module for now.

I have tried using a module, and importing what is defined in the index.d.ts, but TypeScript says I cannot import this file, and that the module name cannot be found. I did learn that if I use a module, it should be accompanied with a loader, such as CommonJS, RequireJS or AMD, but I would prefer to avoid that for now (if it's not a horrible practice to do so, as I want to minimize the levels of complexity for now).

I've tried looking at other projects, but they all use a loader, which seems a bit overkill for this kind of script.

Is there a good way to go about this?

2

There are 2 answers

6
bingles On BEST ANSWER

If you want to make things work without a module loader, you will have to make a few changes. First of all it's worth mentioning that TypeScript supports 2 primary ways of modularizing code, namespaces and modules.

NOTE: A slightly confusing nuance here is that the keywords namespace and module can be used interchangebly and don't by themselves determine whether a thing is a module or a namespace.

1) modules (formerly called external modules) - these use typical import, export, and require semantics and require a module loader and preferably a bundler of some sort. For this pattern, each file is considered a module. Any file that contains any import or export statements will be considered an external module by the TypeScript compiler.

2) namespaces (formerly called internal modules) - this pattern supports namespaces that can span multiple files. This is the module type that you would need to use if you don't want to use a module loader. Usage of this pattern is becoming less common, but it is still an option if you want to use it for whatever reason. To use this type of modularization, you can't have any import or export statements in the file.

Here's the docs on namespaces https://www.typescriptlang.org/docs/handbook/namespaces.html

Assuming you want to go with your original plan, you will need to tweak a couple of things in your code.

1) As mentioned by @sbat, your index.d.ts is re-declaring your namespace and class types. This is likely causing duplicate definition errors. You can replace the entire contents with a simple override of the JQuery interface.

/** Extend the JQuery interface with custom method. */
declare interface JQuery {
    TestNS: () => TestNS.TestClass
}

2) Make sure you don't have any top level exports or imports. Specifically you will want to remove the export from your namespace in test.ts. This will make your module an internal one instead of an external one.

namespace TestNS {
    export class TestClass {
        ...
    }
}
0
sbat On

Assuming you want to just try TypeScript in a simple project, I'd suggest you simplify things a bit:

  • Remove index.d.ts. Definition files describe existing javascript code, so that TypeScript is aware of types in it. You don't have this situation in your project, there is no need to repeat type information you have already in your test.ts, you can safely remove this file.

  • Just build your test.ts into test.js. Make sure your jquery .d.ts is located where it should be.

  • Remove page.js, just code everything in test.ts for a test. Once you make it work, you can reorganize it.

However, if your goal is to have your code modular and extensible, the recommended approach is to indeed use modules:

Starting with ECMAScript 2015, modules are native part of the language, and should be supported by all compliant engine implementations. Thus, for new projects modules would be the recommended code organization mechanism.

https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html

To use modules in production environment in the browser you will need:

  • Bundler. A tool that will call TypeScript to build your individual TS modules, and then combine the build output into single javascript file. You will then include this single file into your HTML. WebPack is probably the easiest to start: https://webpack.js.org/concepts/

  • Package manager. A tool that installs and manages versions of the third-party modules. https://www.npmjs.com/

This is a big area to study, but I don't think it is an overkill. If your code is modular (even if it is just 2 modules), you need an infrastructure to bundle and load your modules one way or the other. There are several options with different benefits and trade-offs, but there is fundamentally no way around that.