-
Notifications
You must be signed in to change notification settings - Fork 904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
improve acorn-walk types, add ".d.ts tests" using tsd
#1344
base: master
Are you sure you want to change the base?
improve acorn-walk types, add ".d.ts tests" using tsd
#1344
Conversation
This makes two changes to the TS types for acorn-walk. **Using `AnyNode` in more places** We currently have the discriminated union `AnyNode`, who's getting used in the functions who accept a visitor object. It enables us to receive the actual Node type as an argument to our visitor function: ``` walk.simple(ast, { Literal: (n) => { // works because our visitor function receives a node of type: // Extract<acorn.AnyNode, { type: "Literal" }> console.log(n.value) } }) ``` If we weren't using the discriminated union, `n` would be of type `Node`, and when we try to access `n.value`, we'd get the TS error `TS2339: Property value does not exist on type Node`. With this change, we now use `AnyNode` in the other places where we receive a Node in a callback, like `findNodeAt`: ``` walk.findNodeAt(ast, null, null, (_, n) => { return n.type === "Literal" && n.raw?.includes("someSubstring") }) ``` With the discriminated union, once we confirm `n.type === "Literal"`, TS can "know" that `n` is a `Literal`, and we can access its fields appropriately. This doesn't change `Node` to `AnyNode` in the parameters to the walker functions themselves, since we don't use TS in the library itself, so we have no need to disambiguate between input Nodes. If we pass e.g. a `BlockStatement` as the first argument to `simple`, that's fine for TS because a `BlockStatement` is a `Node`. I don't think it would hurt anything to change those occurrences to `AnyNode` too, though. **Cleaning up some duplication** The visitor object types previously contained a little duplication because visitor objects can contain either Node type names (like `Literal`) or Aggregate type names (like `Pattern`) as keys. Previously, we used an intersection type (`{} & {}`) to account for the Node/Aggregate cases, but now there's a single conditional type who we can share between the visitor object types. Hopefully giving names to all the different parts makes things clearer too. Also, with `NodeOrAggregateTypeName` representing both sets of type names, we can make the typing a little more accurate for the functions where we receive the type name in a callback. **Adding type definition tests** As part of making the change, I wrote some "type definition tests" using the [`tsd` library](https://github.com/tsdjs/tsd), where we can assert things like "when we write a visitor function for the Node type `Literal`, the walker function will call it with a `Literal` as the first argument." I found them helpful in giving me some confidence while making the changes (just as regular tests do), so I feel like they're worth having if we ever need to change the types. We can run the same test with both the `walk.d.ts` before and after the changes, to see which new scenarios are accounted for with the changes. I excluded the test file from ESLint; we could add it, it would just require us adding the [TS ESLint plugin](https://typescript-eslint.io/getting-started/legacy-eslint-setup/), but I wasn't sure if we wanted that (I noticed the `test` directory is also currently excluded from ESLint). We could also make the test run as part of CI. It runs with `npx tsd` in the acorn-walk directory and uses the test in the `test-d` directory to test `dist/walk.d.ts` by default.
For the Where we use the visitor object Where we call all the functions who accept a visitor object, we couldn't do something like:
...for two reasons: the test logic isn't actually getting executed, just type-checked, so we wouldn't be able to have imperative logic like that, but even if we could, the visitor object isn't always in the same argument position for the different functions. In our mock visitor functions who check the discriminated union behavior, we couldn't extract the functions like:
...because the type of the input parameter |
For me, the |
I agree about the That's the primary "type behavioral" change (to invent a term 😃) in the PR: to make the other callback types pass There's one smaller "type behavioral" change: in places where we pass a Node/Aggregate type name to the user, we'll use a type who reflects this constraint ( Here's a TS Playground link who demonstrates those two changes. We can also visualize the changes using the As for the visitor object types ( It's only so that we can open the d.ts file and read:
rather than:
...where the two possibilities of keys (Node type names and Aggregate type names) are expressed through the "helper type" I'm not sure if breaking out the callback types into their own helper types (e.g.
|
The changes to functions like Could you reduce the scope of this PR to just those |
This makes two changes to the TS types for acorn-walk.
Using
AnyNode
in more placesWe currently have the discriminated union
AnyNode
, who's getting used in the functions who accept a visitor object. It enables us to receive the actual Node type as an argument to our visitor function:If we weren't using the discriminated union,
n
would be of typeNode
, and when we try to accessn.value
, we'd get the TS errorTS2339: Property value does not exist on type Node
.With this change, we now use
AnyNode
in the other places where we receive a Node in a callback, likefindNodeAt
:With the discriminated union, once we confirm
n.type === "Literal"
, TS can "know" thatn
is aLiteral
, and we can access its fields appropriately.This doesn't change
Node
toAnyNode
in the parameters to the walker functions themselves, since we don't use TS in the library itself, so we have no need to disambiguate between input Nodes. If we pass e.g. aBlockStatement
as the first argument tosimple
, that's fine for TS because aBlockStatement
is aNode
. I don't think it would hurt anything to change those occurrences toAnyNode
too, though.Cleaning up some duplication
The visitor object types previously contained a little duplication because visitor objects can contain either Node type names (like
Literal
) or Aggregate type names (likePattern
) as keys. Previously, we used an intersection type ({} & {}
) to account for the Node/Aggregate cases, but now there's a single conditional type who we can share between the visitor object types. Hopefully giving names to all the different parts makes things clearer too.Also, with
NodeOrAggregateTypeName
representing both sets of type names, we can make the typing a little more accurate for the functions where we receive the type name in a callback.Adding type definition tests
As part of making the change, I wrote some "type definition tests" using the
tsd
library, where we can assert things like "when we write a visitor function for the Node typeLiteral
, the walker function will call it with aLiteral
as the first argument."I found them helpful in giving me some confidence while making the changes (just as regular tests do), so I feel like they're worth having if we ever need to change the types. We can run the same test with both the
walk.d.ts
before and after the changes, to see which new scenarios are accounted for with the changes.I excluded the test file from ESLint; we could add it, it would just require us adding the TS ESLint plugin, but I wasn't sure if we wanted that (I noticed the
test
directory is also currently excluded from ESLint).We could also make the test run as part of CI. It runs with
npx tsd
in the acorn-walk directory and uses the test in thetest-d
directory to testdist/walk.d.ts
by default.