-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
why are the ESTree types distinct from ESTree? #413
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
Comments
The answer is - because we have to. For example, imagine that you have a base set of types as follows: interface Bar {
prop: string
}
interface Foo {
prop: Bar
} If we want to extend these to add additional fields, we can do that easily: import { Foo } from 'base';
interface Foo {
newProp: number
}
// valid TS
const x: Foo = {
prop: {
prop: '',
},
newProp: 1,
}; This is fine if you are solely adding new fields. interface DoWhileStatement {
type: 'DoWhileStatement';
test: Expression;
body: Statement;
} Note that the We cannot extend the base types to add our own ones into the unions: type Expression = Foo | Bar
// Duplicate identifier 'Expression'.
type Expression = Foo | Bar | Baz We cannot override the properties on the estree node to make it consider our unions: import { Node } from 'base';
interface Node {
// Subsequent property declarations must have the same type. Property 'body' must be of type 'Expression', but here has type 'Expression | Baz'.
body: Expression | Baz
} So there is no way to extend the base ESTree type definitions such that their intersections include our new typescript node types. This means that we have to redefine every single ESTree node such that they include our special intersection types (i.e. https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/typescript-estree/src/ts-estree/ts-estree.ts) This problem then bubbles up into the As part of doing this, we looked at how we wanted contributors to write rules and what optional features we wanted people to use. Previously we were using an eslint plugin (eslint-plugin-eslint-plugin) to enforce this, but considering we now have complete control of the types, we instead enforced this at compile time by crafting the definitions such that they match exactly how we want our rules to look and what features we want people to use.
We use an enum because it gives very helpful typeahead/intellisense for consuming/comparing node types. enum StrEnum {
Value = "Value"
}
const isEqual = StrEnum.Value === 'Value'; // === true
If there are any nodes that use string constants, raise an issue, or a PR - and let us know. I'm pretty sure we had them all correct.
#330 |
ESLint compatibilityThe core problem seems to be that the ESTree types are not exactly extensible and were probably not designed with that in mind. This is why TypeScript uses keyof in its DOM types, for example, as it can then provide an interface you can extend. Being that we have no such extensible type from ESTree, you have been forced into having to reimplement everything (a really not-so-nice thing to do, but there was no choice, I see). We should consider contributing to the ESTree types so this can be fixed rather than doing such an unfortunate workaround. EnumsMy point was more that it is inconsistent with how ESTree types work. But being that the two can't be mixed anyway because of this implementation, I guess it isn't important and the ability to get good completions is nice. PluginsWe're currently trying to move several plugins over to typescript-eslint, and this isn't possible yet due to the closed types. If you need any help just point at the issues and we can try contribute. |
The problem is that this is not a case of poorly defined type defs in
enum Foo {
Bar = 'Bar'
}
interface ESTree {
type: 'Bar'
}
interface TSESTree {
type: Foo
}
const x: TSESTree = {
type: Foo.Bar,
};
const y: ESTree = x; // no error The thing is, however, that you'll get errors when assigning the real types together because of extra(/missing) properties, or because the intersection types don't match up. The entire reason that we chose to use an enum is because it ensures that even if you somehow coerce the type of the property to string, you still get compile-time safety. enum Foo {
Bar = 'Bar'
}
interface TSESTree {
type: Foo
}
const node: TSESTree = {
type: Foo.Bar,
}
const nodeType: string = node.type;
if (nodeType === 'BarTypo') {
// compile-time safe
}
if (nodeType === Foo.BarTypo) {
// fails at compile time
} It also means you have concrete links to when the specific node type string is used via typescript's "Find Usages" feature (rather than doing a fuzzy search across the codebase for
There aren't that many eslint plugins that are built around the parser's nodes that are also written in typescript - it's a small fraction of a fraction of the user base. Having let the types sit for about 2 months now, the types themselves seem sound (nobody's been fighting or complaining about them within the plugin context that I've seen), so it's a good time to go back and clean them up. I'm back from holidays now, so once I catch up on all of the PRs and issues from when I was away, I'll be working on the refactor to clean up and expose the types. |
I'm currently having a stab at moving dtslint over to use this project so we can finally get it off tslint, so it would be great if we could get those duplicated types exposed. That way even if its a bit unfortunate they have to exist, at least they can be used. About modifying the ESTree types... this is kinda why the core TS lib types use maps: interface Foo {
type: keyof FooTypeMap
}
interface FooTypeMap {
'my-type': MyType
} As you can augment/extend This only solves a couple of problems, though. It wouldn't directly help with the intersection issue (the fact that an What you already have is fine, it would just be nice if there was a solution to allow the base types to be extended. |
Looks like this issue should be closed? There's a merged PR from back in April |
cc @bradzacher — GitHub will only auto-close the issues if you put # Works
fixes #123, fixes #456
# Doesn’t work
fixes #123, #456 |
Thanks! Yeah, I figured that out the hard way shortly after those PRs. Looks like these issues got missed in the manual cleanups james and I did after closing the PRs I'd done that on. Kind of lame by github that they don't have support for it though! |
For some reason, when you refactored to typescript everywhere, the choice was made to write an entire set of unconventional AST types rather than extending ESTree/ESLint types.
Out of interest, why was this?
This leads to all sorts of weirdness when mixing ESLint types in. You get away with it here because you literally rewrote all of ESLint's types and use those instead (from a pseudo-module?).
I do see the comments at the top of that file, too. But avoiding compromising type compatibility with ESLint entirely seems like it should have outweighed your need for "better control".
context.report
a "TSESTree" node if we're strictly implementing ESLint's types as we should (rather than your cloned types)RuleModule
isn't an ESLintRuleModule
Surely you would have been better off extending the ESTree types and being compatible with ESLint, being that you're implementing an ESLint plugin after all.
is there some reason you went down this path?
We are struggling to move our plugins over to typescript-eslint from the obsolete tslint project because of this.
The text was updated successfully, but these errors were encountered: