benjamn / ast-types

Esprima-compatible implementation of the Mozilla JS Parser API

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cannot build identifier with TypeScript typeAnnotation

brieb opened this issue · comments

commented

Trying to build an identifier with a TypeScript typeAnnotation throws an error because the expected type of the type annotation is TypeAnnotation | null, as defined in the flow defs

ast-types/def/flow.js

Lines 209 to 210 in f4a7c6d

def("Identifier")
.field("typeAnnotation", or(def("TypeAnnotation"), null), defaults["null"]);

For example, if I parse const x: string and look at the AST for the identifier I see

{
  "type": "Identifier",
  "name": "x",
  "typeAnnotation": {
    "type": "TSTypeAnnotation",
    "typeAnnotation": {
      "type": "TSStringKeyword"
    }
  }
}

But if I try to create this with a builder like so

astTypes.builders.identifier.from({ name: 'x', typeAnnotation: astTypes.builders.tsTypeAnnotation(astTypes.builders.tsStringKeyword()) })

I get the error

Uncaught Error: {typeAnnotation: [object Object], loc: null, type: TSTypeAnnotation, comments: null} does not match field "typeAnnotation": TypeAnnotation | null of type Identifier

I'll work on a fix for this and submit a PR

Ahh, I think def/typescript.js needs to redefine the fields of the Identifier type, like def/flow.js is doing, to include a typeAnnotation field with type TSTypeAnnotation | null.

One wrinkle: we import both def/flow.js and def/typescript.js by default in main.js, so there's some risk here of one clobbering the other. I guess what we really want is a typeAnnotation field with type TypeAnnotation | TSTypeAnnotation | null, which may require adding some functionality to the type system (in lib/types.js) for getting the current type of a field, so the redefinition code can just add another | option to the existing type (if any), rather than blindly redefining the field.

Another option would be to introduce a super-type of TypeAnnotation and TSTypeAnnotation, but it feels weird to be merging the Flow and TypeScript type systems together like that.

Kind of annoying how Flow claimed a bunch of un-prefixed type names (like TypeAnnotation) whereas (the Babel implementation of) TypeScript makes it clear that TSTypeAnnotation is TypeScript-specific.

commented

Worked on a fix for this yesterday and just put up a PR here #299

There were actually more cases than Identifier, so I fixed those as well.

One wrinkle: we import both def/flow.js and def/typescript.js by default in main.js, so there's some risk here of one clobbering the other.

That's what I was running into with jscodeshift, because it uses what's exported from main.
https://github.com/facebook/jscodeshift/blob/595b15cda4f52673cad8fe3ae65c12e334966dbf/src/core.js#L161-L165
https://github.com/benjamn/recast/blob/7bcc3507adf8c6fba5ff9c48cec61c4a48446dab/lib/types.js#L5

They do indeed clobber each other. Putting the typeAnnotation field in just typescript.js with TSTypeAnnotation breaks flow.js and vice versa.

which may require adding some functionality to the type system (in lib/types.js) for getting the current type of a field

Oh interesting. Something like this?

def("Identifier")
  .field("typeAnnotation", prev => or(prev, def("TSTypeAnnotation")))

How would we handle default values?

Unless we converted all the defs to use this chaining, I'd worry people would accidentally not realize they'd need to use this field declaration form in certain cases.

Alternatively, could we do the merging behind the scenes? Are there any cases where we rely on the overwriting behavior? Or would it be safe to say that if a field is defined multiple times, the type should be or-ed together? We would still run into the question of how to handle default values though.

Another option would be to introduce a super-type of TypeAnnotation and TSTypeAnnotation, but it feels weird to be merging the Flow and TypeScript type systems together like that.

Had a similar idea in #299, creating a bridge layer. Didn't like how it merges the Flow and TypeScript type systems together like you're saying. But, seemed like a reasonable option. We've done or(def("TypeAnnotation"), def("TSTypeAnnotation"), null) outside of def/flow.js and def/typescript.js before, so there is some precedent for this approach.
https://github.com/brieb/ast-types/blob/f4a7c6db8e0eed1f25971d4fcbb84843c83ad615/def/es6.js#L20-L21

That super-type would be defined in a shared place and it wouldn't make sense for either flow.js or typescript.js to use it directly because it affects both. So, seems like we'd end up making a shared file if we did that too.

Kind of annoying how Flow claimed a bunch of un-prefixed type names (like TypeAnnotation) whereas (the Babel implementation of) TypeScript makes it clear that TSTypeAnnotation is TypeScript-specific.

Ya... I guess they got in first.