Can Typescript interfaces be automatically created from files using a webpack loader?

I am currently working on developing a webpack loader that can transform a file containing API data structure descriptions into TypeScript interfaces.

Specifically, the file format I am using is JSON, but this detail should not be crucial as the file serves as a common source of information outlining the communication between web application backend(s) and frontend(s). In the sample code provided below, you will notice that the JSON file includes an empty object to emphasize that the content of the file does not impact the issue at hand.

My current implementation is encountering two errors (with the assumption that the first error is leading to the second one):

[at-loader]: Child process failed to process the request:  Error: Could not find file: '/private/tmp/ts-loader/example.api'.
ERROR in ./example.api
Module build failed: Error: Final loader didn't return a Buffer or String

What steps can I take to generate TypeScript code using a webpack loader?

package.json

{
  "name": "so-example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "awesome-typescript-loader": "^3.2.3",
    "typescript": "^2.6.1",
    "webpack": "^3.8.1"
  }
}

webpack.config.js

const path = require('path');

module.exports = {
  entry: './index.ts',
  output: {
    filename: 'output.js',
  },
  resolveLoader: {
    alias: {
      'my-own-loader': path.resolve(__dirname, "my-own-loader.js"),
    },
  },
  module: {
    rules: [
      {
        test: /\.api$/,
        exclude: /node_modules/,
        use: ["awesome-typescript-loader", "my-own-loader"],
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: "awesome-typescript-loader",
      },
    ]
  },
};

my-own-loader.js

module.exports = function(source) {
  return `
interface DummyContent {
    name: string;
    age?: number;
}
`;
};

index.ts

import * as foo from './example';

console.log(foo);

example.api

{}

I understand that there are alternative methods for generating code, such as converting JSON files to TypeScript using a build tool and then committing them. However, I am interested in exploring a more dynamic solution.


my-own-loader.js does not export json but string.

Indeed, similar to how loading an image file might not always result in exporting binary data but instead produce a JavaScript representation of metadata about the image.

Why do you need to define TypeScript interfaces from a JSON file? Will these interfaces be utilized during TypeScript compilation?

Yes, the goal is to import a file detailing the API data structures and automatically derive the corresponding TypeScript interfaces. By using a shared file, both frontend(s) and backend(s) can maintain consistency on the expected data format.

Answer №1

First of all, I commend you for providing an MCVE. This is a truly intriguing question. The code used to compile this answer is derived from the provided MCVE and can be accessed here.

File Missing?

The error message stating that the file is missing is extremely unhelpful. Despite the file being clearly present in its location, TypeScript refuses to load anything without an acceptable extension.

This misleading error masks the true underlying issue, which is

TS6054: File 'c:/path/to/project/example.api' has unsupported extension. The only supported extensions are '.ts', '.tsx', '.d.ts', '.js', '.jsx'.

You can confirm this by manually adding the file into typescript.js, although it may involve some cumbersome detective work (found at line 95141 in v2.6.1).

for (var _i = 0, rootFileNames_1 = rootFileNames; _i < rootFileNames_1.length; _i++) {
    var fileName = rootFileNames_1[_i];
    this.createEntry(fileName, ts.toPath(fileName, this.currentDirectory, getCanonicalFileName));
}
this.createEntry("c:/path/to/project/example.api", ts.toPath("c:/path/to/project/example.api", this.currentDirectory, getCanonicalFileName));

It may seem like just passing a string between loaders conceptually, but surprisingly, the file name does play a crucial role here.

Potential Resolution

I did not see a straightforward solution using awesome-typescript-loader. However, if you are open to utilizing ts-loader, you can indeed generate TypeScript from files with unconventional extensions, compile it, and inject it into your output.js.

ts-loader offers an option called appendTsSuffixTo which can aid in bypassing the common file extension dilemma. Your webpack configuration might resemble something similar to this setup if you choose this path:

rules: [
  {
    test: /\.api$/,
    exclude: /node_modules/,
    use: [
      { loader: "ts-loader" },
      { loader: "my-own-loader" }
    ]
  },
  {
    test: /\.tsx?$/,
    exclude: /node_modules/,
    loader: "ts-loader",
    options: {
      appendTsSuffixTo: [/\.api$/]
    }
  }
]

Interface Considerations and Developer Experience

Interfaces are removed during compilation. This can be illustrated by running tsc against assets like this

interface DummyContent {
    name: string;
    age?: number;
}

vs. this

interface DummyContent {
    name: string;
    age?: number;
}

class DummyClass {
    printMessage = () => {
        console.log("message");
    }
}

var dummy = new DummyClass();
dummy.printMessage();

To enhance developer experience, consider writing these interfaces to a file solely in the development environment. There's no need to include them in production builds or version control repositories.

Developers likely require them written out for their IDE to properly function. You could append *.api.ts to .gitignore and omit them from the repository, though developers will likely need them within their workspace.

For instance, in my sample repo, a new developer would have to execute npm install as usual and then run npm run build to generate the interfaces locally and eliminate any red underlines.

Answer №2

Although this question is quite old, I recently encountered similar issues, making it still relevant.

An alternative solution involves adding

declare module "*.api";
to your index.d.ts file. However, this approach comes with a major drawback of sacrificing type safety since items in a shorthand module declaration are assigned the type any. This essentially nullifies the purpose of generating TypeScript interfaces from the start.

I found a workaround that seemed to work, even though I don't fully comprehend its inner workings.

package.json

Please note that I've utilized the most recent versions of all components at the time of writing, opting for ts-loader over the outdated awesome-typescript-loader.

{
  "name": "so-example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "ts-loader": "^9.2.8",
    "typescript": "^4.6.3",
    "webpack": "^5.70.0",
    "webpack-cli": "^4.9.2"
  }
}

webpack.config.ts

The necessity of using appendTsSuffixTo is well-documented and vital, but not entirely sufficient on its own.

I unearthed the thinly documented resolveModuleName option for ts-loader. By delving into the depths of the ts-loader codebase and closely examining input-output relationships within the function, I devised the custom resolver function below. Adding a .ts extension to the

resolvedFileName</code was necessary to deceive the TypeScript compiler into validating the file.</p>
<p>Note that this <code>ts-loader
configuration must be applied to both rules, which led me to encapsulate it within a variable.

// webpack config setup
const path = require('path');

const tsLoader = {
  loader: "ts-loader",
  options: {
    appendTsSuffixTo: [/\.api$/],
    resolveModuleName: (moduleName, containingFile, compilerOptions, compilerHost, parentResolver) => {
      if (/\.api$/.test(moduleName)) {
        const fileName = path.resolve(path.dirname(containingFile), moduleName);
        return {
          resolvedModule: {
            originalFileName: fileName,
            resolvedFileName: fileName + '.ts',
            resolvedModule: undefined,
            isExternalLibraryImport: false,
          },
          failedLookupLocations: [],
        };
      }
      return parentResolver(moduleName, containingFile, compilerOptions, compilerHost);
    },
  },
};

module.exports = {
  entry: './index.ts',
  output: {
    filename: 'output.js',
  },
  resolveLoader: {
    alias: {
      'my-own-loader': path.resolve(__dirname, "my-own-loader.js"),
    },
  },
  module: {
    rules: [
      {
        test: /\.api$/,
        exclude: /node_modules/,
        use: [tsLoader, "my-own-loader"],
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: [tsLoader]
      },
    ]
  },
};

tsconfig.json

It's unclear why this step is imperative, but without it, an error message stating

TS18002: The 'files' list in config file 'tsconfig.json' is empty.
arises.

{}

my-own-loader.js

I included a demonstration to corroborate our retention of type safety.

module.exports = function(source) {
  return `
export interface DummyContent {
    name: string;
    age?: number;
}

export const DUMMY_VALUE: DummyContent = {
    name: "Jon Snow",
    age: 24,
}
`;
};

index.ts

Notice how I import the file featuring the .api extension. While it may be feasible to tweak the resolveModuleName function to operate sans extension, I opted not to pursue this route. I prefer having the extension as a visual indicator denoting something unique is happening here.

import { DUMMY_VALUE } from './example.api';

console.log(DUMMY_VALUE.name); // Compiles successfully
console.log(DUMMY_VALUE.youKnowNothing); // Does not compile

Answer №3

If you have an API that adheres to the swagger spec, there is a handy npm package called swagger-ts-generator that allows you to automatically generate TypeScript files from it:

Generate TypeScript Code from Swagger

This node module helps in creating TypeScript code for Angular (version 2 and above) by utilizing Web API metadata in Swagger v2 format.

All you need to do is provide it with the swagger URL, and it will do the heavy lifting of generating TypeScript files. While the examples provided are geared towards Gulp, they can easily be adapted for WebPack as well:

var swagger = {
    url: 'http://petstore.swagger.io/v2/swagger.json',
    //url: 'http://127.0.0.1/ZIB.WebApi.v2/swagger/docs/v1',
    swaggerFile: folders.swaggerFolder + files.swaggerJson,
    swaggerFolder: folders.swaggerFolder,
    swaggerTSGeneratorOptions: {
        modelFolder: folders.srcWebapiFolder,
        enumTSFile: folders.srcWebapiFolder + 'enums.ts',
        enumLanguageFiles: [
            folders.srcLanguagesFolder + 'nl.json',
            folders.srcLanguagesFolder + 'en.json',
        ],
        modelModuleName: 'webapi.models',
        enumModuleName: 'webapi.enums',
        enumRef: './enums',
        namespacePrefixesToRemove: [
        ],
        typeNameSuffixesToRemove: [
        ]
    }
}

Answer №4

My modified version of Mike Patrick's code offers several enhancements.

Instead of exporting a type or interface from the loader, I export an "abstract class."

This results in a better developer experience.

  • No need to import any concrete class/variable
  • Correct typings are provided on the first build
  • However, creating a globals.d.ts is necessary

Check out my fork here!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Fixing Typescript assignment error: "Error parsing module"

Trying to assign an object to the variable initialState, where the type of selectedActivity is Activity | undefined. After using the Nullish Coalescing operator (??), the type of emptyActivity becomes Activity. However, upon execution of this line, an err ...

What is the standard "placeholder" for a Select-box in Angular?

Currently in the process of developing a front-end web application with Angular 6, I have encountered a challenge. Specifically, I am working on creating a component that includes various select-boxes, resembling this setup: https://i.sstatic.net/6DmL9.pn ...

There is no module factory available for the dependency type: ContextElementDependency in Angular

When I run ng build on my Angular 4 project, I encounter the following error: 14% building modules 40/46 modules 6 active ...es\@angular\http\@angular\http.es5.js An error occurred during the build: Error: No module factory availa ...

How can generics be utilized to automatically determine the second argument of my function based on the optional property of the first argument?

Struggling to make TypeScript infer the second argument based on the type of property "data" in the first argument? Looking for tips on setting up type DialogHavingData. type DialogHavingData<T> = /* Need help with this part */ 'data' ...

Challenges with Websocket connectivity following AKS upgrade to version 1.25/1.26

We currently have a Vue.js application running on AKS with no issues on version 1.23, although some webpack/websocket errors are showing up in the console. However, after upgrading our AKS Cluster to either version 1.25 or 1.26, even though the pods are a ...

The button click function is failing to trigger in Angular

Within my .html file, the following code is present: The button labeled Data Import is displayed.... <button mat-menu-item (click)="download()"> <mat-icon>cloud_download</mat-icon> <span>Data Imp ...

Error TS2339: Cannot access attribute 'reactive_support' on interface 'LocalizedStringsMethods'

Encountering the error TS2339: Property 'reactive_support' does not exist on type 'LocalizedStringsMethods' I recently updated TypeScript from version 2.6 to 2.9, and attempted import LocalizedStrings from 'react-localization&apo ...

Mapped Generics in Typescript allows you to manipulate and

Currently, I am attempting to utilize TypeScript generics in order to transform them into a new object structure. Essentially, my goal is to change: { key: { handler: () => string }, key2: { hander: () => number }, } to: ...

Angular - enabling scroll position restoration for a singular route

Is there a way to turn off scroll restoration on a specific page? Let's say I have the following routes in my app-routing.module.ts file... const appRoutes: Routes = [{ path: 'home', component: myComponent}, { path: 'about', compon ...

The Angular material checkbox has a mind of its own, deciding to uncheck

I am having an issue with a list displayed as checkboxes using angular-material (Angular 7). Below, I will provide the code snippets for both the .html and .ts files. Every time I click on a checkbox, it gets checked but then immediately becomes unchecked ...

Using Typescript to create a mapped type that allows for making all properties read-only, with the exception of

I encountered a problem where I didn't want to repeatedly rewrite multiple interfaces. My requirement is to have one interface with full writing capabilities, while also having a duplicate of that interface where all fields are set as read-only excep ...

Encountering "TS1110 Type expected." error in Visual Studio 2015 when utilizing numeric literal type union alias

Currently, I am using: Visual Studio 2015 .NET Framework 4.5.2 I have decided to refactor some of my client-side code from JavaScript to TypeScript. Here's how I started the process: Added a .ts file to my web project Let Visual Studio set up its ...

Async function causing Next JS router to not update page

I'm diving into the world of promises and JavaScript, but I've encountered an issue while working on a registration page following a tutorial on YouTube. Currently, I am using next.js with React and TypeScript to redirect users to the home page i ...

What are the steps to configure options in the ng2-date-picker component?

My current project involves using the ng2-date-picker Angular 2 date picker. I'm struggling with setting options such as minDate, maxDate, dateFormat, and others. Any assistance on how to configure these would be greatly appreciated. Sample code: &l ...

"Encountering a module not found error with an npm package in a React app, however, it is

While attempting to incorporate a react component package for presenting CSV data in a table format, I stumbled upon active-table-react. My React application was created using the CRA and typescript template. Upon adding the npm package, importing the comp ...

Library for Resizing Elements in Angular 18 with AngularResize Plugin

Recently, I encountered an issue with the external library angular-resize-event. Originally, my project was in Angular 15, but as part of an upgrade process, I am now migrating it to version 18. Upon upgrading to Angular 16, I ran into a complication - s ...

Steps to create a personalized loading screen using Angular

I am looking to enhance the loading screen for our angular 9 application. Currently, we are utilizing <div [ngClass]="isLoading ? 'loading' : ''> in each component along with the isloading: boolean variable. Whenever an API ...

Manipulating an Array of Objects based on conditions in Angular 8

I have received an array of objects from an API response and I need to create a function that manipulates the data by enabling or disabling a flag based on certain conditions. API Response const data = [ { "subfamily": "Hair ...

Differentiate between function and object types using an enum member

I'm currently experimenting with TypeScript to achieve narrowed types when dealing with index signatures and union types without explicitly discriminating them, such as using a switch case statement. The issue arises in the code snippet below when at ...

What is the process of setting a TypeScript variable to any attribute value?

I have a variable declared in my TypeScript file public variableName: something; I want to use this variable to replace the value of custom attributes in my HTML code <input type="radio" name="someName" id="someId" data-st ...