How to solve “TS2307: Cannot find module” when using inline webpack loaders with TypeScript

Trying to use a webpack raw-loader for an HTML file inline resulted in an error: TS2307: Cannot find module. Here’s how to fix it!

I’m using web components a lot! I really prefer them for frameworks. They are simple, native and easy to use. I also like typescript and webpack. Combining them is a joy.

Thing is – I’d like to separate my html files from my ts files. I will them import the html as a raw string to add to my component’s innerHTML (or shadowDOM).

I’ll first install the loader: yarn add -D raw-loader. Once done, I can use it in my code.

So my code would look kind of like this:

import './app.element.scss';
import * as template from '!!raw-loader!./app.element.html';

export class AppElement extends HTMLElement {
  public static observedAttributes = [];

  connectedCallback() {
    const title = 'my-component';
    this.innerHTML = template.default.replace('${title}', title);
  }
}

customElements.define('yonatan-root', AppElement);

Note that we are using template.default here because we are importing a module that doesn’t have an explicit default export. In this case, it is translated to { default: <our content> }.

In a naive setup it will fail on this:

TS2307: Cannot find module '!!raw-loader!./app.element.html'

That’s because typescript needs a module definition. Let’s set one up. Create a file called raw-loader.d.ts and paste the following code:

declare module '!!raw-loader!*' {
  const contents:{default: string}
  export = contents
}

Note that here we set the contents as {default: string} because we expect to receive a module that has a default property.

We will now use it in our tsConfig.json and add a line inside our compilerOptions:

"compilerOptions": {
  "types": ["raw-loader.d.ts", "node"]
},

Trying to compile now will work as expected.

If we’d like to make our code a bit nicer, we can use the allowSyntheticDefaultImports compiler option.

In our tsConfig.json we will add allowSyntheticDefaultImports: true to the compilerOptions and our code can now look like this:

import './app.element.scss';
import template from '!!raw-loader!./app.element.html';

export class AppElement extends HTMLElement {
  public static observedAttributes = [];

  connectedCallback() {
    const title = 'my-component';
    this.innerHTML = template.replace('${title}', title);
  }
}

customElements.define('yonatan-root', AppElement);

The differences are bold and underlined. We are now using the default import syntax. This also results in the removal of the .default from the template usage.

This will now fail, because we’ve set our raw-loader module to return a module. We will just replace {default: string} with string and we’re good to go:

declare module '!!raw-loader!*' {
  const contents: string
  export = contents
}

Here’s the full solution in a gist:

<h1>Welcome to ${title}</h1>
import './app.element.scss';
import template from '!!raw-loader!./app.element.html';
export class AppElement extends HTMLElement {
public static observedAttributes = [];
connectedCallback() {
const title = 'My Component';
this.innerHTML = template.replace('${title}', title);
}
}
customElements.define('yonatan-root', AppElement);
view raw app.element.ts hosted with ❤ by GitHub
declare module '!!raw-loader!*' {
const contents: string
export = contents
}
view raw raw-loader.d.ts hosted with ❤ by GitHub
{
"compilerOptions": {
"types": ["raw-loader.d.ts", "node"],
"allowSyntheticDefaultImports": true
}
view raw tsconfig.json hosted with ❤ by GitHub

Final words

If you found this helpful, I’ve written an article on how to tweak webpack configuration to do the same without inline loaders.

Sign up to my newsletter to enjoy more content:

Leave a Reply

Your email address will not be published. Required fields are marked *