Description
Before You File a Proposal Please Confirm You Have Done The Following...
- I have searched for related issues and found none that match my proposal.
- I have searched the current rule list and found no rules that match my proposal.
- I have read 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
While no-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 allow declare 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:
function Mixin(BaseClass) {
return class Mixed extends BaseClass { ... }
}
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.
declare class Mixed { ... }
type AnyClass = new (arg0: never, ...args: never[]) => object;
function Mixin<BaseClass extends AnyClass>(BaseClass: BaseClass): BaseClass & Mixed;
For those who want to comply with this rule, some fake-value classes can be successfully turned into types, though it is more verbose:
declare class SomeClass {
static staticProp: number;
instanceProp: number;
}
Can be converted to:
interface SomeClassConstructor {
staticProp: number;
new (): SomeClass;
}
interface SomeClass {
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 write typeof 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 is not 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 more awkward when the base class expression is complex like mixins, especially when generic passthrough is involved:
type AnyClass = new (...args: any[]) => object;
function Mixin<BaseClass extends AnyClass>(BaseClass: BaseClass) {
return class extends BaseClass { ... }
}
declare class Generic<T> {
prop: T;
}
class Mixed<T> extends Mixin(Generic)<T> { ... }
You can transform Mixed
to:
interface MixedConstructor {
new <T>(): Mixed<T>;
}
type Mixed<T> = typeof Mixin<typeof Generic<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
declare abstract class AnyError extends Error {
constructor(arg0: never, ...args: never[]);
}
Pass
declare abstract class AnyError extends Error {
constructor(arg0: never, ...args: never[]);
}
type ErrorClasses = Array<typeof AnyError>;
Additional Info
No response