javascriptjquerytypescriptnode-modulesjavascript-namespaces

General TypeScript usage for modules / namespaces


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?


Solution

  • 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 {
            ...
        }
    }