How to Fix bundle TypeScript files in ASP.NET CORE 8.0: A Step-by-Step Guide

Introduction

This is a useful post if you want to start adding TypeScript files to your application and bundle them all into a single file.

Understanding the Bug

If you want to bundle multiple files into one file and you look at TypeScript documentation, you will see you have only one option:

{
  "compilerOptions": {
      "module": "system",
      "noImplicitAny": true,
      "removeComments": true,
      "preserveConstEnums": true,
      "outFile": "../../built/local/tsc.js",
      "sourceMap": true
  },
  "include": [
      "src/**/*"
  ],
  "exclude": [
      "node_modules",
      "**/*.spec.ts"
  ]
}

I made an example on GitHub on this branch: tscSystem-bad

The creation of "tsc.js" involves obtaining all TS files either by date of modification or alphabetically, then transforming each TS file into JS code.

This solution will work very well until you use derived classes. The problem is that when you use the derived class, you will encounter this: ReferenceError: Cannot access 'Filter' before initialization

Error

Analyzing the error

This is the bundle file "tsc.js" generated by the module system. The problem is that the Filter is created after the FilterUsers.

Solution: Using TypeScript Module

A solution is available on GitHub with an application on the main branch: ASP.NET Core Web App (Model-View-Controller).

Steps:

  1. Add this NuGets:
  1. Add a "TypeScript JSON Configuration File" with the name: tsconfig.json

  2. Change the content of the tsconfig.json with this:

This configuration will allow you to have modules.

{    
  "compileOnSave": true,
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "module": "ES2020",
    "importHelpers": false,
    "isolatedModules": true,
    "noEmitHelpers": true,
    "noEmitOnError": true,
    "noImplicitAny": true,
    "outDir": "./wwwroot/js/",
    "rootDir": "./wwwroot/ts",
    "moduleResolution": "Node",
    "removeComments": false,
    "sourceMap": true,
    "strict": true,
    "target": "ES2020"
  },
  "include": [
    "./wwwroot/ts/**/*"
  ],
  "exclude": []
}
  1. In Program.cs, we will add these lines:
builder.Services.AddBundling()
    .UseDefaults(builder.Environment)
    .AddEcmaScript();
app.UseBundling(
    bundles =>
    {
        bundles.AddJs("/BoostrapperExample.js")
            .Include("/js/BoostrapperExample.js")
            .EnableEs6ModuleBundling();
    });
  1. Add this line in the property group in the csproj:
<BundleOnBuild>true</BundleOnBuild>

Now, when we build the project, we will see the TS files converted to JS files.

  1. Now we add three classes to demonstrate that the solution is working.
export class Filter {
    public showMessage() :void {
        console.log("Filter class");
    }
}

We want to import Filter into our class, "UserFilter" and derive from that class:

import { Filter } from "./Filter.js";

export class FilterUsers extends Filter {

    public showUser() {
        console.log("User Filter");
    }
}

This class has the purpose of creating the UserFilter and calling both methods from the parent and derived class.

import { UserFilter } from "./Services/UserFilter.js";

class BoostrapperExample {

    constructor() {
        let userFilter = new FilterUsers();

        userFilter.showMessage();
        userFilter.showUser();
    }
}

new BoostrapperExample();

7) To check if it's working, add this line to the index.cshtml file. This path is virtual and this folder "bundles" will be created at runtime in the browser.

<script src="~/bundles/BoostrapperExample.js"></script>

Conclusion

By using modules, the solution ensures that dependencies are properly managed, avoiding common pitfalls associated with simple concatenation methods. This approach is highly recommended for maintaining scalable and error-free codebases.