Using the TypeScript AST

Updated: 24 June 2026

Generating Typescript using AST’s

Some parsers other that the Typescript one is Esprima, Acorn, these are Javascript Parsers> Other languages also have parsers as well as tools called Parser Generators that enable you to generate a parser for a given language or usecase

Before getting started, you should first install the typescript package into your project with:

Terminal window
1
yarn add typescript

Using the typescript compiler you are able to parse TS into an AST as well as build code using the TS AST

You can also generate the ts. code using the Typescript to AST Converter

Generate Types

1
export type User = {
2
name: string
3
age: number
4
}

To generate a type like above using the TS compiler you can use the following:

1
import ts, { factory } from 'typescript'
2
import { writeFileSync } from 'fs'
3
4
// create name property
5
const nameProp = factory.createPropertySignature(
6
undefined,
7
factory.createIdentifier('name'),
8
undefined,
9
factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
10
)
11
12
// create age property
13
const ageProp = factory.createPropertySignature(
14
undefined,
15
factory.createIdentifier('age'),
16
undefined,
17
factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
18
)
19
20
// create User type
21
const userType = factory.createTypeAliasDeclaration(
22
undefined,
23
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
24
factory.createIdentifier('User'),
25
undefined,
26
factory.createTypeLiteralNode([nameProp, ageProp])
27
)

We can then use the generated type to build a more complex type:

1
export type Admin = {
2
user: User
3
}
1
const userProp = factory.createPropertySignature(
2
undefined,
3
factory.createIdentifier('user'),
4
undefined,
5
factory.createTypeReferenceNode(factory.createIdentifier('User'), undefined)
6
)
7
8
const adminType = factory.createTypeAliasDeclaration(
9
undefined,
10
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
11
factory.createIdentifier('Admin'),
12
undefined,
13
factory.createTypeLiteralNode([userProp])
14
)

Next, we’ll creata a NodeArray our of the two type declarations we want in our file like so:

1
const nodes = factory.createNodeArray([userType, adminType])

Printing to Code

We can then print the two types out as Typescript code with the following:

1
const sourceFile = ts.createSourceFile(
2
'placeholder.ts',
3
'',
4
ts.ScriptTarget.ESNext,
5
true,
6
ts.ScriptKind.TS
7
)

The above sourcefile is a way for us to store some basic settings for the file we’re going to be saving our file content into, we’ve got the name placeholder.ts in the above case but this doesn’t really output the file we’re going to be outputing

Next, we can create a Printer instance and use it to generate our output file:

1
const printer = ts.createPrinter()
2
3
const outputFile = printer.printList(ts.ListFormat.MultiLine, nodes, sourceFile)

Lastly, we can print the outputFile using fs:

1
import { writeFileSync } from 'fs'
2
3
//... other code from above
4
5
writeFileSync('./output.ts', outputFile)

Which will output the following:

1
export type User = {
2
name: string
3
age: number
4
}
5
export type Admin = {
6
user: User
7
}

Update 24 June 2026

Here’s a small implementation of a generic type visitor that can be used to scrape files for information using ts-morph

describe.ts

1
/**
2
* Basic pattern for visiting nodes and scraping metadata from them
3
*/
4
import {
5
Project,
6
SyntaxKind,
7
Node,
8
type ImplementedKindToNodeMappings,
9
} from 'ts-morph';
10
11
// this is the data that the visitor needs to return
12
interface Visited {
13
kind: string;
14
name?: string;
15
children?: Visited[];
16
}
17
18
// some helpers to make the tree traversal statically typed
19
type SyntaxKindNames = keyof typeof SyntaxKind;
20
type SyntaxKindFromName<N extends SyntaxKindNames> = (typeof SyntaxKind)[N] &
21
keyof ImplementedKindToNodeMappings;
22
23
type Visitors<T = unknown> = {
24
[K in SyntaxKindNames]: (
25
node: ImplementedKindToNodeMappings[SyntaxKindFromName<K>],
26
) => T | undefined;
27
};
28
29
// add any visitors here as needed
30
const visitors: Partial<Visitors<Visited>> = {
31
Identifier: (node) => {
32
return { kind: node.getKindName(), name: node.getText() };
33
},
34
SyntaxList: (node) => {
35
return {
36
kind: node.getKindName(),
37
name: node.getKindName(),
38
children: visitChildren(node),
39
};
40
},
41
ImportDeclaration: (node) => {
42
return {
43
kind: node.getKindName(),
44
name: node.getModuleSpecifier().getLiteralText(),
45
children: [
46
visitNode(node.getDefaultImport()),
47
...node.getNamedImports().map(visitNode),
48
].filter(exits),
49
};
50
},
51
ImportSpecifier: (node) => {
52
return {
53
kind: node.getKindName(),
54
name: node.getName(),
55
isTypeOnly: node.isTypeOnly(),
56
};
57
},
58
ClassDeclaration: (node) => {
59
return {
60
kind: node.getKindName(),
61
name: node.getName(),
62
children: [
63
...node.getProperties().map(visitNode),
64
...node.getMembers().map(visitNode),
65
],
66
};
67
},
68
ObjectLiteralExpression: (node) => {
69
return {
70
kind: node.getKindName(),
71
children: node.getProperties().map(visitNode),
72
};
73
},
74
PropertyAssignment: (node) => {
75
return {
76
kind: node.getKindName(),
77
name: node.getName(),
78
type: node.getType().getText(),
79
};
80
},
81
PropertyDeclaration: (node) => {
82
return {
83
kind: node.getKindName(),
84
name: node.getName(),
85
isReadonly: node.isReadonly,
86
type: node.getType().getText(),
87
};
88
},
89
MethodDeclaration: (node) => {
90
return {
91
kind: node.getKindName(),
92
name: node.getName(),
93
isReadonly: node.isReadonly,
94
type: node.getType().getText(),
95
};
96
},
97
StringLiteral: (node) => {
98
return {
99
kind: node.getKindName(),
100
value: node.getLiteralValue(),
101
};
102
},
103
};
104
105
function exits<T>(v?: T): v is Exclude<T, undefined> {
106
return typeof v !== 'undefined';
107
}
108
109
function visitNode(c?: Node) {
110
if (!c) {
111
return undefined;
112
}
113
114
const handle = visitors[c.getKindName() as keyof Visitors];
115
116
if (!handle) {
117
console.warn('No handler for', c.getKindName());
118
}
119
120
const typedHandle = handle as (c: Node) => Visited | undefined;
121
122
return typedHandle?.(c);
123
}
124
125
function visitChildren(node: Node) {
126
return node
127
.getChildren()
128
.map(visitNode)
129
.filter((c) => !!c);
130
}
131
132
const path = process.argv[process.argv.length - 1];
133
const project = new Project();
134
135
const sourceFile = project.addSourceFileAtPath(path);
136
137
const result = visitChildren(sourceFile);
138
139
// just print the result as JSON, but this can of course be whatever
140
console.log(JSON.stringify(result));

This can be used by running it directly with node. Piping to jq can also provide some nice highlighting:

Terminal window
1
node describe.ts path/to/file/to/describe/example.ts | jq

References

In addition to using the AST Direcly, some libraries that are also handy for working with the Typescript AST are:

And some more general AST related tools: