Exploring the realm of Shadow DOM and Custom elements, I've encountered an interesting discrepancy between how JavaScript (JS) and TypeScript (TS) handle modular imports. Am I missing something here?
My primary JS file has this structure...
// import classes (and register custom elements)
import { Game } from './engine/game.js';
import { MainMenuState } from './menu/main-menu-state.js';
// retrieve the game custom element from the DOM and instantiate the main menu state
const oGame = document.querySelector('test-game');
const oState = document.createElement('main-menu-state');
// assign the state to the game
oGame.setState(oState);
The module itself is structured 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);
}
At the end of the module, you can see that I'm defining the custom test-game element using the Game class. The main-menu-state element follows a similar pattern.
This setup functions correctly, operating as expected.
However, when transitioning this code into TypeScript, an issue arises.
The main TS file takes this form...
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 TypeScript module exhibiting a familiar layout...
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);
}
Despite the similarity, the browser console shows an error this time...
Uncaught TypeError: oGame.setState is not a function at main.ts:9
My tsconfig file specifies 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 fail to comprehend the reason behind the disparity in behavior unless the TS compiler handles module exports differently.
UPDATE
It seems the issue stems from the TS compiler. By simply checking the object types, the problem appears to be resolved...
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);
}