Library / SDK
benjamn/ast-types avatar
benjamn/ast-types

ast-types: a type system for JavaScript syntax trees

Esprima-compatible implementation of the Mozilla JS Parser API

1,175 stars196 forksTypeScriptMIT

At a glance

What is it?
ast-types is an Esprima compatible implementation of the Mozilla Parser API abstract syntax tree type hierarchy, giving you builders, runtime type checks and a typed visitor for JavaScript ASTs. It does not parse anything, it defines what the nodes are, and its TypeScript declarations are generated from the same definitions as the runtime, which is where its sharpest edge lives.
Who is it for?
Adopt ast-types if you write code that manipulates JavaScript syntax trees and want one node vocabulary that works across parsers, with builders and checks that catch malformed trees at runtime rather than in a transform that silently produces the wrong output. Do not adopt it if all you need is to read a field off a node from a single parser, because the plain AST from that parser is enough.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A type system, not a parser

ast-types does not read JavaScript. The package description says what it is: an Esprima compatible implementation of the Mozilla JS Parser API, described in the README as an efficient, modular implementation of the abstract syntax tree type hierarchy pioneered by the Mozilla Parser API.

That distinction is the whole design. A parser gives you a tree for one particular source of truth, and the shape of that tree reflects whichever parser produced it. ast-types supplies the vocabulary instead: a named type for every node, a definition of which fields that node has, which of them are required, and what values they accept. With those definitions in hand you can build a node correctly, check that a node is what you think it is, and walk a tree knowing which visitor method to call for which node type.

It also means the library is parser agnostic in practice. The test suite is built around a set of parsers listed as development dependencies, including esprima, espree, the Babel parser and flow-parser, which is how a hierarchy aimed at Esprima compatibility stays usable with other tools.

The target audience is anyone writing a transform, a codemod or a linter rule in TypeScript or JavaScript, where a mistake in node shape produces a broken output file rather than an exception.

Installing it and the declaration story

Installation is a single npm command:

bash
npm install ast-types

The README is explicit that the package ships its own TypeScript declarations and that there is no @types/ast-types to install. TypeScript 4.4 and later are supported, and the declarations are type-checked against 4.4, 4.9 and 5.x on every commit, so the published types are not a best effort transcription that drifts from the runtime.

That last point comes from the build. The package manifest shows a gen script that runs ts-node over script/gen-types.ts, a build step that is plain tsc, and a test script that regenerates the types, builds, and then runs script/run-tests.sh. The prepare script cleans the lib/ directory and rebuilds, so a published package is always built from generated output rather than a checked in lib/ directory. The manifest also declares the module entry points, main at lib/main.js and types at lib/main.d.ts, with an exports map that lets consumers reach into lib/ paths directly.

To work on the library itself, the README gives the clone and test sequence:

bash
git clone https://github.com/benjamn/ast-types.git
cd ast-types
npm install
npm test

Note that npm test regenerates the types and rebuilds before running the suite, so a type change is tested end to end rather than only at runtime.

builders and namedTypes: making a node and checking it

The everyday API is two namespaces. namedTypes gives you the runtime type checks, and builders gives you constructors for the nodes. The README's basic example builds a small if statement and then asserts on it:

js
import assert from "assert";
import {
  namedTypes as n,
  builders as b,
} from "ast-types";

var fooId = b.identifier("foo");
var ifFoo = b.ifStatement(fooId, b.blockStatement([
  b.expressionStatement(b.callExpression(fooId, []))
]));

assert.ok(n.IfStatement.check(ifFoo));
assert.ok(n.Statement.check(ifFoo));
assert.ok(n.Node.check(ifFoo));

The hierarchy is the point of those last three assertions. An IfStatement is a Statement, and a Statement is a Node, and the checks compose that way, so a single check call tells you whether a value belongs to a family of node types rather than only whether it is exactly one shape.

The example also shows why builders exist. b.callExpression(fooId, []) takes a callee and an argument list and produces a structurally valid node, including the details you would otherwise forget, which is precisely the class of bug that produces a transform that runs without error and writes out invalid code.

The TypeScript surface mirrors the runtime one. The declarations are generated from the same definitions, so every node type is available as a TypeScript type of the same name under namedTypes, and NodePath and Type are generic, which lets a visitor narrow path.node for you.

getFieldNames knows the fields a node does not have

The library also exposes the field level primitives, and the example the README gives makes the capability obvious. Given a partial object that is not really a complete node:

js
import {
  getFieldNames,
  getFieldValue,
} from "ast-types";

const partialFunExpr = { type: "FunctionExpression" };

// Even though partialFunExpr doesn't actually contain all the fields that
// are expected for a FunctionExpression, types.getFieldNames knows:
console.log(getFieldNames(partialFunExpr));
// [ 'type', 'id', 'params', 'body', 'generator', 'expression',
//   'defaults', 'rest', 'async' ]

The field list is derived from the type definition, not from the object's own properties, so a half built node still reports the full expected field set. getFieldValue goes further and returns the default for a field that is not defined, which the README shows by asking for the generator flag of that same partial object and getting false back.

That behaviour is what makes the two higher level helpers possible. eachField and someField are defined in terms of getFieldNames and getFieldValue, and the README's own copy of them is short enough to be worth reading once, because it shows the contract: every field of the node's type is visited, including those that are missing or undefined, and if the object has no corresponding definition the callback is never called.

The README then uses exactly that to copy a node, substituting default values where the source node had none, which is a subtle behaviour to inherit rather than reimplement.

visit, NodePath and the traverse contract

Traversal is the reason most people reach for this library, and the visitor has a contract worth understanding before you write one.

A visitor method is called for a node of the matching type and receives a single argument, a NodePath wrapping the node of interest. The README's example flags the use of arguments.callee anywhere in a tree:

js
visit(ast, {
  // This method will be called for any node with .type "MemberExpression":
  visitMemberExpression(path) {
    var node = path.node;

    if (
      n.Identifier.check(node.object) &&
      node.object.name === "arguments" &&
      n.Identifier.check(node.property)
    ) {
      assert.notStrictEqual(node.property.name, "callee");
    }

    // It's your responsibility to call this.traverse with some
    // NodePath object (usually the one passed into the visitor
    // method) before the visitor method returns, or return false to
    // indicate that the traversal need not continue any further down
    // this subtree.
    this.traverse(path);
  }
});

Two responsibilities land on you. You must call this.traverse(path) before returning, or the subtree below the current node is never visited, and the alternative is to return false to stop the descent explicitly. Forget the traverse call and a visitor silently inspects only the top of the tree, which is the kind of bug that shows up as a rule that never fires.

The other point is that method names are matched by type, and the README notes that types.visit precomputes a lookup table from every known type to the appropriate visitor method, so a handler written for a broad type such as Function also fires for FunctionDeclaration, FunctionExpression and ArrowFunctionExpression.

The union type caveat that pushes narrowing onto you

There is one documented sharp edge, and it is in the TypeScript section rather than the runtime section.

The README states that a node type's TypeScript type describes what the definitions accept, which is often a union. The consequence follows directly: widening a field to admit a node shape a parser really produces is a change consumers may need to narrow around. The CHANGELOG.md is pointed at for the cases where this has already happened.

In practice this means a type level improvement can arrive as a source level change for the code that consumes it. A field typed as a union of node kinds might now include an extra kind, and code that switched exhaustively over that union stops being exhaustive. The types get more accurate, and the burden of handling the extra case moves downstream.

That is a defensible trade-off, and it is the price of generating the declarations from the runtime definitions rather than maintaining them by hand, but it is a cost that belongs in your upgrade planning. A major version bump in this package can mean consumer code changes, and the CHANGELOG is the place to look before you bump the pin.

The related version detail: the package declares support for TypeScript 4.4 and later, with the declarations checked against 4.4, 4.9 and 5.x, while the development dependency is pinned to typescript 4.9.4.

Release pace, the stale engine field and the licence

The repository is not archived and the last push was on 2026-10-12. The release history is dense in a short window: v0.16.2-rc.0 on 2026-08-30 at 18:46, v0.16.2 at 18:59 the same afternoon, and v0.16.3 at 20:26, so a release candidate and two final versions inside two hours.

The version number itself carries information. The library is still on 0.x, which under npm semantics means the API is not treated as stable, and the presence of release candidates shows the project is comfortable shipping pre-releases. For a library that other tools wrap, that is worth weighing against the stability of the type definitions you depend on.

The engines field is the odd one out. The package manifest declares node >=4, a requirement from years ago, while the development dependencies use current tooling and the type checking targets modern TypeScript. The engine floor is not a real constraint on modern Node, but it is a signal that the field has not been revisited.

Licensing is MIT, stated in the LICENSE file and in the package metadata, authored by Ben Newman. A permissive licence on a library that other tools depend on means the licensing risk is low; the upgrade risk sits in the type definitions rather than in the terms.

Editorial conclusion

Adopt ast-types if you write code that manipulates JavaScript syntax trees and want one node vocabulary that works across parsers, with builders and checks that catch malformed trees at runtime rather than in a transform that silently produces the wrong output. Do not adopt it if all you need is to read a field off a node from a single parser, because the plain AST from that parser is enough. Verify first that the parser you use produces a tree this hierarchy recognises, read CHANGELOG.md for the field widening changes your own code may have to narrow around, and check that your TypeScript version is 4.4 or later before relying on the bundled declarations.

Frequently asked questions

Do I need to install @types/ast-types separately?

No. The README states that the package ships its own TypeScript declarations and that there is no @types/ast-types to install. TypeScript 4.4 and later are supported, and the declarations are type-checked against 4.4, 4.9 and 5.x on every commit.

Does ast-types parse JavaScript source code?

No. It is an Esprima compatible implementation of the Mozilla Parser API abstract syntax tree type hierarchy, so it defines node types, builders and checks, and it lets you traverse a tree that a parser produced. The test suite runs against several parsers listed as development dependencies, including esprima, espree, the Babel parser and flow-parser.

Why does my visitor not visit the whole tree?

A visitor method is responsible for calling this.traverse(path) before it returns, otherwise the traversal does not continue into that subtree. The alternative is to return false to stop descending deliberately, as the README explains in its arguments.callee example.

What breaks when I upgrade ast-types?

The README notes that a node type's TypeScript type describes what the definitions accept, which is often a union, and that widening a field to admit a shape a parser really produces can require consumers to narrow around the change. The CHANGELOG.md records the cases where this has happened.

Official sources

  1. benjamn/ast-types on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/benjamn-ast-types.svg)](https://hysenlabs.com/projects/benjamn-ast-types)

Was this useful?