You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: website/blog/typed-napi.md
+51-20Lines changed: 51 additions & 20 deletions
Original file line number
Diff line number
Diff line change
@@ -102,9 +102,10 @@ interface TypeMpa {
102
102
}
103
103
```
104
104
What is `TypeMaps`? It is a type that contains all static node types. It is a map from kind to the static type of the kind.
105
+
Here is a simplified example of the TypeScript static type.
105
106
106
107
```typescript
107
-
typeTypeScriptMap= {
108
+
typeTypeScript= {
108
109
// AST node type definition
109
110
function_declaration: {
110
111
type:"function_declaration", // kind
@@ -135,22 +136,21 @@ Tree-sitter also provides alias types where a kind is an alias of a list of othe
135
136
136
137
We want to both type a node's kind and its fields.
137
138
138
-
## `SgNode<K>`
139
+
## Give a type to `SgNode`
139
140
140
-
`SgNode<M, K>` is the main type in the new API. It is a generic type that represents a node with kind `K`. It is a union of all possible kinds of nodes.
141
+
`SgNode<M, K>` is the main type in the new API. It is a generic type that represents a node with kind `K` of language type map `M`. It is a union of all possible kinds of nodes.
141
142
142
143
```typescript
143
144
classSgNode<MextendsTypesMap, KextendskeyofM> {
144
145
kind:K
145
-
fields:M[K]['fields'] //for simplicity
146
+
fields:M[K]['fields'] //demo definition, real one is more complex
146
147
}
147
148
```
148
149
149
150
150
-
151
151
### `ResolveType<M, T>`
152
152
153
-
TreeSitter's type alias is helpful to reduce the generated JSON file size but it is not useful to users because the alias is never directly used as a node's kind nor is used as `kind` in ast-grep rule.
153
+
TreeSitter's type alias is helpful to reduce the generated JSON file size but it is not useful to users because the alias is never directly used as a node's kind nor is used as `kind` in ast-grep rule. For example, `declaration` mentioned above can never be used as `kind` in ast-grep rule.
154
154
155
155
We need to use a type alias to resolve the alias type to its concrete type.
156
156
@@ -173,11 +173,10 @@ type LowPriorityString = string & {}
173
173
174
174
The above type is a linient string type that is compatible with any string type. But it also uses a well-known trick to take advantage of TypeScript's type priority to prefer the `keyofM` type in completion over the `string& {}` type. To make it more self-explanatory, the `stirng& {}` type is aliased to `LowPriorityString`.
175
175
176
-
Problem? open-ended union is not well supported in TypeScript
We need other tricks to make it work better. Introducing `RefineNode` type.
181
180
182
181
### Bridging general nodes and specific nodes via `RefineNode`
183
182
@@ -196,6 +195,18 @@ Which kind of union should we use?
196
195
197
196
Note `SgNode<'expression'|'type'>` is different from `SgNode<'expression'> |SgNode<'type'>`
198
197
TypeScript has difficulty in narrowing the previous type, because it not safe to assume the former is equivalent to the later.
198
+
199
+
```typescript
200
+
let single:SgNode<'expression'|'type'>
201
+
if (single.kind==='expression') {
202
+
single// Still SgNode<'expression' | 'type'>, not narrowed
203
+
}
204
+
let union:SgNode<'expression'> |SgNode<'type'>
205
+
if (union.kind==='expression') {
206
+
union// SgNode<'expression'>, narrowed
207
+
}
208
+
```
209
+
199
210
However, `SgNode` is covariant in the kind parameter and this means it is okay.
200
211
it is general okay to distribute the type constructor over union type if the parameter is covariant.
201
212
but TypeScript does not support this feature.
@@ -220,22 +231,24 @@ Now let's talk about how to refine the general node to a specific node in ast-gr
220
231
221
232
Most AST traversal methods in ast-grep now can take a new type parameter to refine the node to a specific kind.
222
233
223
-
This is like the `document.querySelector<T>` method in the DOM API. It returns a general `Element` type, but you can refine it to a specific type like `HTMLDivElement` by providing generic argument.
234
+
This is like the `document.querySelector<T>` method in the [DOM API](https://www.typescriptlang.org/docs/handbook/dom-manipulation.html#the-queryselector-and-queryselectorall-methods). It returns a general `Element` type, but you can refine it to a specific type like `HTMLDivElement` by providing generic argument.
224
235
225
-
For example `sgNode.find<"KIND">()`
236
+
For example `sgNode.parent<"KIND">()`. This will refine the node to a specific kind.
226
237
227
-
This uses the intersting overloading feature of TypeScript
238
+
This uses the interesting overloading feature of TypeScript
228
239
229
240
```typescript
230
-
interfaceNodeMethod<K> {
241
+
interfaceNodeMethod<M, K> {
231
242
():SgNode
232
-
<TextendsK>():SgNode<T>
243
+
<TextendsK>():RefineNode<M, T>
233
244
}
234
245
```
246
+
If no type is provided, it returns a general node, `SgNode<M>`.
247
+
If a type is provided, it returns a specific node, `SgNode<M, K>`.
235
248
236
-
If no type is provided, it returns a general node. If a type is provided, it returns a specific node.
249
+
The reason why we use two overloading signatures here is to distinguish the two cases. If we use a single generic signature, TypeScript will always return the single version `SgNode<M, K1|K2>` or always returns a union of different `SgNode`s.
237
250
238
-
another way to do runtime checking is via `sgNode.is("kind")`, One time type narrowing
251
+
another way to do runtime checking is via `sgNode.is("kind")`, one time type narrowing
239
252
240
253
```typescript
241
254
if (sgNode.is("function_declaration")) {
@@ -250,6 +263,10 @@ The key feature of the new API is to automatically refine the node to a specific
250
263
This is done by using the `field` method
251
264
252
265
`sgNode.field("kind")` will automatically check the field name and its corresponding types in the static type, and refine the node to the specific kind.
266
+
```typescript
267
+
let exportStmt:SgNode<'export_statement'>
268
+
exportStmt.field('declaration') // refine to SgNde<'function_declaration'> | SgNode<'variable_declaration'> ...
269
+
```
253
270
254
271
255
272
### Exhaustive Checking via `sgNode.kindToRefine`
@@ -291,8 +308,12 @@ This is also the reason why we need to include `string` in the `Kinds`.
291
308
292
309
### Opt-in refinement for better compile time performance
293
310
294
-
The new API is designed to provide a better type checking and completion experience to the user. But it comes with a cost of performance. The more type information the user provides, the slower the compile time.
311
+
The new API is designed to provide a better type checking and completion experience to the user. But it comes with a cost of performance.
312
+
One type map for a single language can be several thousand lines of code with hundreds of kinds.
313
+
The more type information the user provides, the slower the compile time.
295
314
315
+
316
+
So you need to explicitly opt in type information by passing type parameters to `parse` method.
The last feature worth mentioning is the typed rule! You can even type the `kind` in rule JSON!
306
327
328
+
You can look up the available kinds in the static type via the completion popup in your editor. (btw I use nvim)
307
329
```typescript
308
330
sgNode.find({
309
331
rule: {
@@ -313,11 +335,20 @@ sgNode.find({
313
335
})
314
336
```
315
337
338
+
```typescript
339
+
interfaceRule<M> {
340
+
kind:Kinds<M>
341
+
...// other rules
342
+
}
343
+
```
344
+
316
345
## Ending
317
346
318
347
I'm very thrilled to see the future of AST manipulation in TypeScript.
319
348
This feature enables users to switch freely between untyped AST and typed AST.
320
349
350
+
To use a quote from [Theo's video](https://www.youtube.com/clip/Ugkxn2oomDuyQjtaKXhYP1MU9TLEShf5m1nf):
351
+
352
+
> There are very few devs that understands Rust deeply enough and compiler deeply enough that also care about TypeScript in web dev enough to build something for web devs in Rust
321
353
322
-
https://x.com/hd_nvim/status/1868453729940500924
323
-
There are very few devs that understands Rust deeply enough and compiler deeply enough that also care about TypeScript in web dev enough to build something for web devs in Rust
354
+
ast-grep will strive to be the one that bridges the gap between Rust and TypeScript.
0 commit comments