Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
Description
Before You File a Proposal Please Confirm You Have Done The Following...
- I havesearched for related issues and found none that match my proposal.
- I have searched thecurrent rule list and found no rules that match my proposal.
- I haveread the FAQ and my problem is not listed.
My proposal is suitable for this project
- I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).
Link to the rule's documentation
https://eslint.org/docs/latest/rules/no-unused-vars
Description
Whileno-unused-var
triggering on most values that are only used in types makes sense, this breaks down when doing advanced things with classes. I propose an option to allowdeclare class
expressions to be counted as used by types.
I run into this frequently when writing declaration files, specifically because I write type-only subclasses to widen the class (the most frequent example is to create a bound that allows abstract classes and changed constructors). I recognize this is niche, a more universal example would be typing JS mixins or in general functions that return internally-scoped classes.
For example if you encounter a JS function like:
functionMixin(BaseClass){returnclassMixedextendsBaseClass{ ...}}
It is most nicely typed as:
// Not really defined at the top level of this file but hoisted because there's no way to inline and it'd be really hideous if it did work. Module visibility means that this fake value existing doesn't matter in practice.declareclassMixed{ ...}typeAnyClass=new(arg0:never, ...args:never[])=>object;functionMixin<BaseClassextendsAnyClass>(BaseClass:BaseClass):BaseClass&Mixed;
For those who want to comply with this rule, some fake-value classescan be successfully turned into types, though it is more verbose:
declareclassSomeClass{staticstaticProp:number;instanceProp:number;}
Can be converted to:
interfaceSomeClassConstructor{staticProp:number;new():SomeClass;}interfaceSomeClass{instanceProp:number;}
This already is a mild sacrifice because it's more verbose, forces you to separate instance and static props (even if they make more intuitive sense to order them interspersed), and you lose the ability to writetypeof SomeClass
to mean the class, which to me feels more intuitive if you're used to regular classes.
However there's no valid transformations when:
- The class has an accessibility modifier on a property, i.e.
private
,protected
,readonly
, andinternal
- The class has a truly private property. This may seem unimportant but they have implications towards the variance of the class so stripping them isnot a true 1:1 mapping.
- The class has a getter or setter. While in common cases you could emulate them by creating a property this breaks down when the getter/setter can't be unified (e.g. set
number
, getstring
) and it also breaks subclassing as you can't substitute a getter/setter pair for a property. - The class is abstract.
- The class has an
internal
,private
, orprotected
constructor. You could simply leave out the constructor inSomeClassConstructor
but this gives worse diagnostics.
While I'm quite comfortable with classes and translating them to regular interfaces, I also imagine most people will find the syntax for generic classes annoying. It also becomes moreawkward when the base class expression is complex like mixins, especially when generic passthrough is involved:
typeAnyClass=new(...args:any[])=>object;functionMixin<BaseClassextendsAnyClass>(BaseClass:BaseClass){returnclassextendsBaseClass{ ...}}declareclassGeneric<T>{prop:T;}classMixed<T>extendsMixin(Generic)<T>{ ...}
Youcan transformMixed
to:
interfaceMixedConstructor{new<T>():Mixed<T>;}typeMixed<T>=typeofMixin<typeofGeneric<T>>&{ ...};
This code is obviously ugly and it doesn't generalize well to more complex mixins, especially where the generic parameters and parameters differ significantly.
Fail
declareabstractclassAnyErrorextendsError{constructor(arg0:never, ...args:never[]);}
Pass
declareabstractclassAnyErrorextendsError{constructor(arg0:never, ...args:never[]);}typeErrorClasses=Array<typeofAnyError>;
Additional Info
No response