nxwebpack

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

YK
Yonatan Kra

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 over 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 then 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:

<div class="gist-file" translate="no" data-color-mode="light" data-light-theme="light">
  <div class="gist-data">
    
<div itemprop="text"
  class="Box-body p-0 blob-wrapper data type-html  "
  style="overflow: auto" tabindex="0" role="region"
  aria-label="app.element.html content, created by YonatanKra on 06:14AM on September 08, 2021."
>

    
<h1>Welcome to ${title}</h1>
</div>
  </div>
  <div class="gist-meta">
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16/raw/fffef4a14021d1294646f06a22d579cdd7d19101/app.element.html" style="float:right" class="Link--inTextBlock" target="_blank" rel="noopener">view raw</a>
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16#file-app-element-html" class="Link--inTextBlock" target="_blank" rel="noopener">
      app.element.html
    </a>
    hosted with &#10084; by <a class="Link--inTextBlock" href="https://github.com" target="_blank" rel="noopener">GitHub</a>
  </div>
</div>
<div class="gist-file" translate="no" data-color-mode="light" data-light-theme="light">
  <div class="gist-data">
    
<div itemprop="text"
  class="Box-body p-0 blob-wrapper data type-typescript  "
  style="overflow: auto" tabindex="0" role="region"
  aria-label="app.element.ts content, created by YonatanKra on 06:14AM on September 08, 2021."
>

    
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);
</div>
  </div>
  <div class="gist-meta">
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16/raw/fffef4a14021d1294646f06a22d579cdd7d19101/app.element.ts" style="float:right" class="Link--inTextBlock" target="_blank" rel="noopener">view raw</a>
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16#file-app-element-ts" class="Link--inTextBlock" target="_blank" rel="noopener">
      app.element.ts
    </a>
    hosted with &#10084; by <a class="Link--inTextBlock" href="https://github.com" target="_blank" rel="noopener">GitHub</a>
  </div>
</div>
<div class="gist-file" translate="no" data-color-mode="light" data-light-theme="light">
  <div class="gist-data">
    
<div itemprop="text"
  class="Box-body p-0 blob-wrapper data type-typescript  "
  style="overflow: auto" tabindex="0" role="region"
  aria-label="raw-loader.d.ts content, created by YonatanKra on 06:14AM on September 08, 2021."
>

    
declare module '!!raw-loader!*' {
const contents: string
export = contents
}
</div>
  </div>
  <div class="gist-meta">
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16/raw/fffef4a14021d1294646f06a22d579cdd7d19101/raw-loader.d.ts" style="float:right" class="Link--inTextBlock" target="_blank" rel="noopener">view raw</a>
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16#file-raw-loader-d-ts" class="Link--inTextBlock" target="_blank" rel="noopener">
      raw-loader.d.ts
    </a>
    hosted with &#10084; by <a class="Link--inTextBlock" href="https://github.com" target="_blank" rel="noopener">GitHub</a>
  </div>
</div>
<div class="gist-file" translate="no" data-color-mode="light" data-light-theme="light">
  <div class="gist-data">
    
<div itemprop="text"
  class="Box-body p-0 blob-wrapper data type-json-with-comments  "
  style="overflow: auto" tabindex="0" role="region"
  aria-label="tsconfig.json content, created by YonatanKra on 06:14AM on September 08, 2021."
>

    
{
"compilerOptions": {
"types": ["raw-loader.d.ts", "node"],
"allowSyntheticDefaultImports": true
}
</div>
  </div>
  <div class="gist-meta">
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16/raw/fffef4a14021d1294646f06a22d579cdd7d19101/tsconfig.json" style="float:right" class="Link--inTextBlock" target="_blank" rel="noopener">view raw</a>
    <a href="https://gist.github.com/YonatanKra/e1a00241db14bfbd7571f7b498972c16#file-tsconfig-json" class="Link--inTextBlock" target="_blank" rel="noopener">
      tsconfig.json
    </a>
    hosted with &#10084; by <a class="Link--inTextBlock" href="https://github.com" target="_blank" rel="noopener">GitHub</a>
  </div>
</div>

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.

Comments

Yonatan Kra

Thanks for the heads up.
Mind sharing your use case? I could not reproduce the error while using this, but it might be our tsconfig files differ in some way.

Type to search articles