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:
| <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); |
| declare module '!!raw-loader!*' { | |
| const contents: string | |
| export = contents | |
| } |
| { | |
| "compilerOptions": { | |
| "types": ["raw-loader.d.ts", "node"], | |
| "allowSyntheticDefaultImports": true | |
| } |
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.

This didn’t quite work for me as written; TypeScript still couldn’t find the module. The following post details some tweaks I had to make to get it to work: https://medium.com/@thisisjimkeller/loading-custom-modules-in-typescript-4-ea9b5293137e
Thanks for the heads up.
Mind sharing your use case? I could not reproduce the error while using this, but it might be our
tsconfigfiles differ in some way.