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://typescript-eslint.io/rules/no-explicit-any/
Description
I'd like to revisitthis issue for adding an option to theno-explicit-any
rule that allows the use ofany
as generic constraint. That issue was seemingly closed by a bot, so I'm not even sure if that issue reached a real consensus. Also, I've written a few code examples that I think add some value to the discussion, which is why I'm opening this issue even if there's technically another existing (closed) issue.
Disclaimer: Before anyone mentions it, I'm aware thatthis PR in the typescript repo exists and would solve all problems below if merged, but It's been around since 2018 and it's unsure when or if it is going to get merged, so I still think this discussion is worth having.
Example 1: Non-exported types in 3rd party packages
When integrating with 3rd party typescript packages, it's unfortunately common for packages to not expose all the types the consumer may need. While this is a fault in those packages, I think we have to be pragmatic and acknowledge that this is a common enough problem to justify a workaround.
Let's imagine the packageexample-package
that doesn't export a generic constraint type:
typeGenerics={a:unknown;b:unknown;c:unknown;};exporttypeFoo<GextendsGenerics>={generics:G;}
This is what it would look like to consume this package:
Strategy 1: Usingnever
as generic constraint
Usingnever
makes the constraint too specific and the function now only acceptsFoo<never>
import{Foo}from"example-package";declarefunctionuseNever<TextendsFoo<never>>(arg:T):void;useNever({generics:{a:123,b:123,c:123}});// ^ Type '{ a: number; b: number; c: number; }' is not assignable to type 'never'.(2322)
Strategy 2: Usingunknown
as generic constraint
Usingunknown
requires us to reconstruct the type used for the generic constraint while usingunknown
everywhere.
declarefunctionuseUnknown<TextendsFoo<{a:unknown,b:unknown,c:unknown}>>(arg:T):void;
This actually works. Neither Typescript or typescript-eslint yields errors, but this is unnecessarily verbose when all I want is to tell typescript thatuseUnknown
should accept any form ofFoo
. It's also brittle, since I have to redefine implementation details of the 3rd party package. If the package refactors their internal type definitions, I will have to refactor my code.
Strategy 3: Usingany
as generic constraint
Finally let's try usingany
. It gives us the best developer experience. It applies the generic constraint we want, is terse and not brittle, and without yielding any typescript errors. However, unfortunately, typescript-eslint complains if you haveno-explicit-any
enabled.
declarefunctionuseAny<TextendsFoo<any>>(arg:T):void;
Example 2: The same problem can still occur even if you own all the type definitions
Let's say we have this builder pattern:
typeGenericKeys="a"|"b"|"c"interfaceGenerics<Aextendsunknown[],Bextendsunknown[],Cextendsunknown[]>{a:A,b:B,c:C,}declareclassBuilder<ValuesextendsGenerics<unknown[],unknown[],unknown[]>>{readonlyvalues:Readonly<Values>;add<AddToextendsGenericKeys,Added>():Builder<{[KinGenericKeys]:KextendsAddTo ?[...Values[K],Added] :Existing[K]}>}constbuilder=newBuilder<{a:[],b:[],c:[]}>().add<"a",number>().add<"a",string>().add<"b",boolean>().add<"b",null>()// ^ Builder<{ a: [number, string]; b: [boolean, null]; c: []; }>
Now we want to define some functions that accepts generic variants of this Builder type. Observe that if we try the same strategies as in Example 1, we'll experience the exact same problems:
Strategy 1: Usingnever
as generic constraint
declarefunctionuseNever<BextendsBuilder<never>>(builder:B):void;useNever(builder);// ^ Argument of type 'Builder<{ a: [number, string]; b: [boolean, null]; c: []; }>' is not assignable to parameter of type 'Builder<never>'
Strategy 2: Usingunknown
as generic constraint
Once again, this works, but is as verbose and brittle as mentioned in Example 1.
declarefunctionuseUnknown<BextendsBuilder<Generics<unknown[],unknown[],unknown[]>>>(builder:B):void;useUnknown(builder)
Strategy 3: Usingany
as generic constraint
Once again,any
as generic constraint is the winner:
declarefunctionuseAny<TextendsFoo<any>>(arg:T):void;
eslint test cases
Fail
// Using `any` outside of generic constraints should still be an errorconstfoo:any=123;functionfn(arg:any){}
Pass
// But using `any` inside a generic constraint should be validfunctionfn<Textendsany>(arg:T){}