Generate TypeScript Declarations from a Flow Codebase

Illustration: Generate TypeScript Declarations from a Flow Codebase

TypeScript is one of the fastest growing static type-checking tools. And with more and more people adopting it, we decided it was just about time we started providing TypeScript declarations. So starting with PSPDFKit for Web 2020.2.3, the pspdfkit package on npm includes TypeScript declarations for our public APIs. This means that from now on, you get optional type checking, richer editor integration, smart autocompletion, and easier code refactoring of our public APIs. This will lead to a better developer experience for our users who use TypeScript.

Ideation

Our internal codebase is written in Flow, so there was no straightforward way to provide TypeScript declarations. The easiest way of generating declarations is to write them manually, but it would have been hard to keep them up to date, as every time a public API changed, we would have had to make sure our declarations were updated. There was also no automatic way of knowing if they needed updating without going through things manually, so this would have been a tedious task.

In order to solve this, we took the approach of generating declarations programmatically (the not-so-straightforward approach). This process involved multiple steps, as shown below.

Implementation

The first step was to make some modifications in the existing codebase so that we could convert it to TypeScript. One example of this is that we used the TimeoutID type in our codebase; this is a type that is globally available in Flow but not available in TypeScript. One of the modification options was to make changes to the source code, but we didn’t want to play with the original code. We instead used jscodeshift to make the changes before converting the code to TypeScript:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const glob = require("glob");
const path = require("path");
const jsc = require("jscodeshift");
const flowParser = require("jscodeshift/parser/flow");

const files = glob.sync("src/**/*.js", {
  root: "./",
  ignore: ["src/**/__tests__/*.js"]
});

files.forEach(file => {
  const flowCode = fs.readFileSync(file, "utf-8");

  const ast = jsc(code, {
    parser: flowParser()
  });

  ast
    .find(jsc.GenericTypeAnnotation, {
      id: { name: "TimeoutID" }
    })
    .replaceWith("number");

  const transformedCode = ast.toSource();
});

The code above shows one of the transformations we did. If you want to play with jscodeshift before using it, you can do it on astexplorer.net.

The transformedCode was now ready to be converted to TypeScript. We used @khanacademy/flow-to-ts for that:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ...other imports.
const convert = require('@khanacademy/flow-to-ts/src/convert');

const files = glob.sync('src/**/*.js', {
  root: './',
  ignore: ['src/**/__tests__/*.js'],
})

files.forEach(file => {
	// ...code to get `transformedCode` mentioned above.
	const typescriptCode = convert(transformedCode, {
	  printWidth: 80,
	  singleQuote: true,
	  semi: false,
	  prettier: true,
	})

	fs.writeFileSync('./typescript', typescriptCode, (err) => {})
}

Once we executed the above code, we placed all the files converted to TypeScript into the typescript folder. Next, we had to generate declaration files from them. We used the typescript package to do that:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ... other imports.
const ts = require("typescript");

// ...generate TypeScript files as mentioned above.

const tsFiles = glob.sync("typescript/**/*.ts");

const options = {
  declaration: true,
  emitDeclarationOnly: true,
  declarationDir: "declarations"
};

const program = ts.createProgram(tsFiles, options);
const res = program.emit();

// In case there was some error while generating declaration,
// throw an error.
res.diagnostics.forEach(({ messageText }) => {
  throw messageText;
});

After this step, we got the TypeScript declarations of all the files that are part of our codebase. The issue here is that we only want to ship the declarations for our public API, and a lot of the declarations generated are not needed by the public API and are useless for the user.

To solve this, we created a dependency tree of all the files, starting from the main entry declaration file. This allowed us to track all the declaration files that were directly or indirectly needed by the main declaration (which only included the public API). We then deleted the declaration files that were not needed, meaning we only shipped the relevant parts of the declarations.

Conclusion

In this blog post, we covered the process of generating TypeScript declarations from a Flow codebase, discussing the approach we took and the reasoning behind it. We hope that the availability of TypeScript declarations in PSPDFKit will make your developer experience even better. If you want to start playing with it, make sure you request a trial.

PSPDFKit for Web

PDF viewing, annotating, and collaboration for web apps.

Try Now