Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to generate Typescript interfaces from files with a webpack loader?

I am attempting to create a webpack loader that converts a file containing a description of API data structures into a set of TypeScript interfaces.

In my concrete case, the file is JSON, but this should be ultimately irrelevant — the file is only a shared source of data describing the interaction between web application backend(s) and frontend(s). In my MCVE below, you can see that the JSON file contains an empty object to underscore how the type and contents of the file do not matter to the problem.

My current attempt reports two errors (I assume the second is caused by the first):

[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

How can I 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 recognize that there are other code generation techniques. For example, I could convert my JSON files to TypeScript with some build tool and check them in. I'm looking for a more dynamic solution.


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

That's correct, much like loading an image file doesn't always export binary data but sometimes outputs a JavaScript data structure representing metadata about the image.

Why you need to define the typescript interfaces from json file? Would the interfaces be used for typescript compilation?

Yes. I want to import a file that describes my API data structures and automatically generate corresponding TypeScript interfaces. Having a shared file allows the frontend(s) and backend(s) to always agree on what data will be present.

like image 503
Shepmaster Avatar asked Nov 09 '17 17:11

Shepmaster


People also ask

Can Webpack config be ts file?

For those of you who like to work with TypeScript, webpack offers the possibility to use a configuration file written in TypeScript. Webpack v5 already ships with TypeScript definitions, so you don't have to install @types/webpack but you need to install typescript, ts-node and @types/node.

Does TSC use Webpack?

Like Babel, Webpack depends on TSC to transpile TypeScript to JavaScript but as TSC doesn't have a clue about Webpack, hence Webpack needs a loader to talk to TSC. This is where the ts-loader comes into play. Webpack compiles a TypeScript file using ts-loader package which asks TSC to do the compilation.

Does ts-loader use TSC?

ts-loader uses tsc , the TypeScript compiler, and relies on your tsconfig. json configuration.

Does TypeScript use Webpack?

Webpack allows TypeScript, Babel, and ESLint to work together, allowing us to develop a modern project. The ForkTsCheckerWebpackPlugin Webpack plugin allows code to be type-checked during the bundling process.


1 Answers

First off, kudos for providing an MCVE. This is a really interesting question. The code I worked with to put this answer together is based on said MCVE, and is available here.

Missing File?

This is a most unhelpful error message indeed. The file is clearly in that location, but TypeScript will refuse to load anything that doesn't have an acceptable extension.

This error is essentially hiding the real error, which is

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

This can be verified by hacking into typescript.js, and manually adding the file. It's ugly, as detective work often is (starts 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));

Conceptually, you're just passing a string between loaders, but it turns out the file name is important here.

A possible fix

I didn't see a way to do this with awesome-typescript-loader, but if you're willing to use ts-loader instead, you can certainly generate TypeScript from files with arbitrary extensions, compile that TypeScript, and inject it into your output.js.

ts-loader has an appendTsSuffixTo option, that can be used to work around the well-known file extension pain. Your webpack config might look something like this if you went that route:

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$/]
    }
  }
]

Note on interfaces and DX

Interfaces are erased by the compiler. This can be demonstrated by running tsc against something 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();

In order to provide a nice developer experience, you may need to write these interfaces to a file in the dev environment only. You don't need to write them out for a production build, and you don't need (or want) to check them into version control.

Developers probably need to have them written out so their IDE has something to sink its teeth into. You might add *.api.ts to .gitignore, and keep them out of the repository, but I suspect they'll need to exist in the developers' workspaces.

For example, in my sample repo, a new developer would have to run npm install (as usual) and npm run build (to generate the interfaces in their local environment) to get rid of all their red squigglies.

like image 52
Mike Patrick Avatar answered Oct 20 '22 19:10

Mike Patrick