souporserious

Generate TypeScript Docs Using TS Morph

4 min read

TypeScript provides rich feedback about how our code should work. In this post, we'll look at using TS Morph to auto-generate docs for functional React components.

A handful of tools for reading types exist in the wild like react-docgen and react-docgen-typescript. These tools work well for the most part but are limited in the features they offer. Building a library to parse all of the possible combinations TypeScript offers is no easy task. Today, we’re going to look at using a tool called TS Morph to see how far we can get reading types with a strong set of utilities.

Follow along at the repo here to see a working example. The author of TS Morph has also provided a fantastic AST viewer that helps show what we can query.

Getting Started

We’ll start by installing the libraries we need, defining a new project, and reading in an example file that exports a React component:

1yarn add typescript ts-morph
1import * as path from 'path'
2import { Project } from 'ts-morph'
3
4const project = new Project({
5 tsConfigFilePath: path.resolve(process.cwd(), 'tsconfig.json'),
6})
7const source = project.getSourceFile(path.resolve(process.cwd(), 'Example.tsx'))
1type ExampleProps = {
2 /* The title of the example. */
3 title?: string
4
5 /* The color of the text. */
6 color?: string
7
8 /* The horizontal alignment of the text. */
9 align?: 'left' | 'center' | 'right'
10}
11
12export function Example({ title, color, align }: ExampleProps) {
13 return (
14 <div
15 style={{
16 color,
17 textAlign: align,
18 }}
19 >
20 {title}
21 </div>
22 )
23}

Querying Functional Components

Now that our project is set up, we can query all of the functions in our file using the getFunctions method on source. In our case, we’re only interested in particular functions which are exported and start with a captial letter since this is how components are defined in JSX:

1const components = source.getFunctions().filter((declaration) => {
2 const name = declaration.getName()
3 const isComponent = name[0] === name[0].toUpperCase()
4 return isComponent && declaration.hasExportKeyword()
5})

Reading Prop Types

Now that we have all of the functions we care about, we can start to iterate through their types to gather the information we want to display:

1const docs = components.forEach((declaration) => {
2 const [props] = declaration.getParameters()
3 const type = props.getType()
4})

Here we’re using the getParameters helper to read the function’s arguments. We pull off the first argument, which in React’s case is the component’s props. Next, we get the type and its properties that are associated with the component props. We’ll use getTypeAtLocation and pass it the declaration to get each prop type:

1const docs = components.map((declaration) => {
2 const [props] = declaration.getParameters()
3 const type = props.getType()
4 const typeProps = type.getProperties().map((prop) => {
5 return {
6 name: prop.getName(),
7 type: prop.getTypeAtLocation(declaration).getText(),
8 }
9 })
10 return {
11 name: declaration.getName(),
12 typeProps,
13 }
14})

Reading Comments

While the name and prop types are helpful, it would be nice to pull in the comments we’ve associated with each type as well. We can do this using getLeadingCommentRanges to read the first comment:

1const docs = components.map((declaration) => {
2 const [props] = declaration.getParameters()
3 const type = props.getType()
4 const typeProps = type.getProperties().map((prop) => {
5 const [propDeclaration] = prop.getDeclarations()
6 const [commentRange] = propDeclaration.getLeadingCommentRanges()
7 return {
8 name: prop.getName(),
9 type: prop.getTypeAtLocation(declaration).getText(),
10 comment: commentRange.getText(),
11 }
12 })
13 return {
14 name: declaration.getName(),
15 typeProps,
16 }
17})

Huge thanks to David for the help here!

That’s it for today! I encourage you to further play around with this example and see if you can query the default value for each type.

Conclusion

There is still a lot more we’d need to do for this to be production-ready, like checking if the function is wrapped in React.forwardRef, determine if the type is imported from another file, among many other edge cases. I’m still learning about this library, but I am excited about the possibilities it brings! I particularly enjoy that the API feels friendly and makes static analysis a lot easier.

Updated:
  • docs
  • typescript
  • react
  • components
Previous post
Better library DX using JSDoc links
Next post
Build Your Own Code Playground
© 2022 Travis Arnold