Search code examples
typescriptruntime-errores6-modulescustom-element

Difference between module imports in JavaScript and Typescript?


I'm dipping my toe into Shadow DOM / Custom elements and think I've found a quirk between how JS and TS handle modular imports or maybe I'm doing something wrong?

My main JS file looks like this...

// import classes (and register custom elements)
import { Game } from './engine/game.js';
import { MainMenuState } from './menu/main-menu-state.js';

// get the game custom element from the DOM and create the main menu state
const oGame = document.querySelector('test-game');
const oState = document.createElement('main-menu-state');

// push the state onto the game
oGame.setState(oState);

With the module looking like this...

export class Game extends HTMLElement {

    constructor() {
        super();
        this._shadowRoot = this.attachShadow({
            mode: 'open'
        });
    }

    setState(oState) { ... }

    pushState(oState) { ... }

    popState() { ... }

}

if(!customElements.get('test-game')) {
    customElements.define('test-game', Game);
}

As you can see at the bottom of the module I'm defining the custom test-game element using the Game class. The main-menu-state element is defined in a similar way.

This works fine and the code executes as I'd expect

Once I'd converted this code into Typescript I run into an issue.

The main TS file looks like this...

import { StateInterface } from './engine/state.interface.js';

import { Game } from './engine/game.js';
import { MainMenuState } from './menu/main-menu-state.js';

const oGame: Game = document.querySelector('test-game') as Game;
const oState: StateInterface = document.createElement('main-menu-state') as MainMenuState;

oGame.setState(oState);

With the TS module looking pretty familiar...

export class Game extends HTMLElement implements GameInterface {

    public constructor() {
        super();

        this._shadowRoot = this.attachShadow({
            mode: 'open'
        });
    }

    public setState(oState: StateInterface): void { ... }

    public pushState(oState: StateInterface): void { ... }

    public popState(): void { ... }

}

if(!customElements.get('test-game')) {
    customElements.define('test-game', Game);
}

But this time the browser console gives me the following error...

Uncaught TypeError: oGame.setState is not a function at main.ts:9

My tsconfig file is set to use ES6 module resolution...

{
    "compilerOptions": {
        "module": "es6",
        "target": "es6",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true,
        "alwaysStrict": true,
        "noUnusedLocals": true,
        "outDir": "./public/js",
        "rootDir": "./public/ts"
    }
}

I just can't understand why there'd be a difference between these two pieces of code unless the TS compiler is doing something different with the module exports


EDIT

Ok, so it looks like this is something funky caused by the TS compiler. Simply type checking the objects seems to straighten it out...

import { StateInterface } from './engine/state.interface.js';

import { Game } from './engine/game.js';
import { MainMenuState } from './menu/main-menu-state.js';

const oGame: Game = document.querySelector('test-game') as Game;
const oState: StateInterface = document.createElement('main-menu-state') as MainMenuState;

if(oGame instanceof Game && oState instanceof MainMenuState) {
    oGame.setState(oState);
}

Solution

  • In TypeScript, when you import something but only use it as a type, TS leaves the import out when it compiles your code. In this case, the import going away means the elements don't get registered, and therefore the methods come up missing when you attempt to query for a custom element. The reason why type-checking the values worked here is because the imported classes are actually being used as values for instanceof, therefore the import stays.

    Here's an issue about it, with some solutions in there as well. For side effects that need to fire off, import './engine/game.js' at the top of your entry JS file will make sure it always runs and registers the element.