8000 [no-explicit-any] False positive for type predicates · Issue #1048 · typescript-eslint/typescript-eslint · GitHub
[go: up one dir, main page]

Skip to content

[no-explicit-any] False positive for type predicates #1048

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

Closed
leximayfield opened this issue Oct 7, 2019 · 5 comments
Closed

[no-explicit-any] False positive for type predicates #1048

leximayfield opened this issue Oct 7, 2019 · 5 comments
Labels
package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin working as intended Issues that are closed as they are working as intended

Comments

@leximayfield
Copy link

Repro

{
	"parser": "@typescript-eslint/parser",
	"extends": [
		"plugin:@typescript-eslint/recommended"
	]
}
interface Foo {
	bar: number;
}

export const isFoo = (x: any): x is Foo => {
	if (typeof x.bar !== 'number') {
		return false;
	}
	return true;
}

Expected Result
No lint issues.

Actual Result

  5:26  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any

Additional Info
An any type should be allowed as a function parameter when writing a type predicate, as it is reasonable for the function to take an any and narrow the type based on some condition inside the function. Typescript 3.7 is going to have type assertion functions, and those should probably also allow the any type.

Versions

package version
@typescript-eslint/eslint-plugin 2.3.2
@typescript-eslint/parser 2.3.2
TypeScript 3.6.3
ESLint 6.5.1
node 12.11.0
npm 6.11.3
@leximayfield leximayfield added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Oct 7, 2019
@bradzacher bradzacher added working as intended Issues that are closed as they are working as intended and removed triage Waiting for team members to take a look labels Oct 7, 2019
@bradzacher
Copy link
Member
bradzacher commented Oct 7, 2019

There's no false positive here.
Your code is super unsafe, and the lint rule is eluding to that fact.

What happens if I pass isFoo(null)? Your code will crash.
So the any type is completely wrong - because your code doesn't handle any value.

I haven't spent enough time around narrowing unknown to an object type, but this code will work and uses zero anys.

interface Foo {
	bar: number;
}

export function isObject(x: unknown): x is Record<string, unknown> {
  return typeof x === 'object' && x != null;
}

export function isFoo(x: unknown): x is Foo {
  if (!isObject(x)) {
    return false;
  }

  if (typeof x.bar !== 'number') {
    return false;
  }
  
  return true;
}

Also - this type guard still isn't completely safe, because you will potentially allow more properties than Foo defines in: isFoo({bar: 1, baz: false}) === true.


In strictly typed code, you shouldn't ever really have to reach for any. unknown and never should do everything you want in typesafe ways.

@leximayfield
Copy link
Author

What happens if I pass isFoo(null)? Your code will crash.

Yikes, you're completely right, null hadn't occurred to me and because it was any the type system was incapable of helping me out. I'll use unknown and friends from now on. Thanks.

@keithlayne
Copy link

I will generally write the original example as

interface Foo { bar: number }
export const isFoo = (x: any): x is Foo => 
  typeof x === 'object' && 
  x && // or often x !== null if I want to be more explicit
  typeof x.bar === 'number'

That's the only time I use any in general, and it's not because I want to, it's because of ergonomics. In the third step above (if you use unkown instead of any) x will be narrowed to object. That's the friction point here.

And just to be clear, I only do this with type guards for object types, but object types constitute 99% of all the custom type guards I write. If object were treated like Record<PropertyKey, unknown> then everything would probably be great, but it's not, and you can't do anything useful with object without a type assertion.

I agree in principle with this lint rule. x as never is no better than x as any in real life. But the point is well taken that using unknown forces you to explicitly add an annotation to make everything work. You're still doing something potentially unsafe, but you're assuming responsibility to do so correctly. Just like I take full responsibility when I write a type guard using any.

I still think this is a valid rule, but not everyone will want to apply it in typeguards. I felt like the conversation around the original example got kinda muddied by the error in it even though it showed a great argument against using any.

Also - this type guard still isn't completely safe, because you will potentially allow more properties than Foo defines in: isFoo({bar: 1, baz: false}) === true.

One last thing, and this is important. In fact, if you return false in that case, then you are lying to TS and you wlll create unsoundness and errors in your program. This is because TS is structurally typed. Any type predicate returning x is Foo must return true for any object type that has a number-valued foo property, and false otherwise. This is pretty much because if it returns false TS will treat it as not having that property. There is currently no way to express x is exactly Foo in the type system.

@bradzacher
Copy link
Member
bradzacher commented Oct 8, 2019

I agree that using never isn't a good idea. At the time, I couldn't think of a better way to do it - if you know how to convert object to an index signature, I'd love to know.
I just thought of a better way which works around the problem completely, and updated the comment appropriately.
This removes the unsafety from the type, and eliminates the problem of having to deal with object.

Though I would argue that having an expression-level as never (or as any) assertion is significantly safer than having an argument-level : any type. Especially when the former is surrounded by if's etc like it was.


IMO, it's not a good idea to add an option to this rule to allow an any in a specific case, unless it's required to make sure that typescript compiles.
I think it's much better to use eslint disable comments on a case-by-case basis, so that they are easier to notice in a code review, and can be discussed appropriately.


I wouldn't say it's necessarily less sound, it's just something to be aware of, and it's something you probably need to be handling via 3.7 assertion functions instead of pure type guards.

For example, I've seen a lot of code which assumes exact objects and throws on unexpected properties (code that handles configuration objects is a good example of this).

I do wish TS would add exact objects though. They're one of the few things I really love about flow.

@keithlayne
Copy link

Yeah, totally agree on expression-level assert vs any arg. That's what I was trying to say, just not very well :)

Pretty much agree with everything you're saying here. I hope/assume TS will keep improving around some of these pain points.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin working as intended Issues that are closed as they are working as intended
Projects
None yet
Development

No branches or pull requests

3 participants
0