-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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://typescript-eslint.io/rules/no-explicit-any/
Description
I'd like to revisit this issue for adding an option to the no-explicit-any
rule that allows the use of any
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 that this 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 package example-package
that doesn't export a generic constraint type:
type Generics = { a: unknown; b: unknown; c: unknown; };
export type Foo<G extends Generics> = {
generics: G;
}
This is what it would look like to consume this package:
Strategy 1: Using never
as generic constraint
Using never
makes the constraint too specific and the function now only accepts Foo<never>
import { Foo } from "example-package";
declare function useNever<T extends Foo<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: Using unknown
as generic constraint
Using unknown
requires us to reconstruct the type used for the generic constraint while using unknown
everywhere.
declare function useUnknown<T extends Foo<{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 that useUnknown
should accept any form of Foo
. 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: Using any
as generic constraint
Finally let's try using any
. 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 have no-explicit-any
enabled.
declare function useAny<T extends Foo<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:
type GenericKeys = "a" | "b" | "c"
interface Generics<
A extends unknown[],
B extends unknown[],
C extends unknown[]
> {
a: A,
b: B,
c: C,
}
declare class Builder<Values extends Generics<unknown[], unknown[], unknown[]>> {
readonly values: Readonly<Values>;
add <AddTo extends GenericKeys, Added>(): Builder<{
[K in GenericKeys]: K extends AddTo ? [...Values[K], Added] : Existing[K]
}>
}
const builder = new Builder<{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: Using never
as generic constraint
declare function useNever<B extends Builder<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: Using unknown
as generic constraint
Once again, this works, but is as verbose and brittle as mentioned in Example 1.
declare function useUnknown<B extends Builder<Generics<unknown[], unknown[], unknown[]>>>(builder: B): void;
useUnknown(builder)
Strategy 3: Using any
as generic constraint
Once again, any
as generic constraint is the winner:
declare function useAny<T extends Foo<any>>(arg: T): void;
eslint test cases
Fail
// Using `any` outside of generic constraints should still be an error
const foo: any = 123;
function fn (arg: any) { }
Pass
// But using `any` inside a generic constraint should be valid
function fn<T extends any>(arg: T) { }