8000 [no-unnecessary-type-assertion] only checks non-null-assertion · Issue #4485 · typescript-eslint/typescript-eslint · GitHub
[go: up one dir, main page]

Skip to content

[no-unnecessary-type-assertion] only checks non-null-assertion #4485

8000
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
3 tasks done
c-vetter opened this issue Jan 24, 2022 · 7 comments · Fixed by #8558
Closed
3 tasks done

[no-unnecessary-type-assertion] only checks non-null-assertion #4485

c-vetter opened this issue Jan 24, 2022 · 7 comments · Fixed by #8558
Labels
accepting prs Go ahead, send a pull request that resolves this issue enhancement New feature or request locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@c-vetter
Copy link
c-vetter commented Jan 24, 2022
  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have read the FAQ and my problem is not listed.

Repro

{
	"root": true,
	"parser": "@typescript-eslint/parser",
	"parserOptions": {
		"ecmaVersion": 2021,
		"project": "./tsconfig.json",
		"sourceType": "module"
	},
	"plugins": [
		"@typescript-eslint"
	],
	"rules": {
		"@typescript-eslint/no-unnecessary-type-assertion": "error"
	}
}
// code.ts
type Foo = 3

const foox: Foo = 3 as 3
const fooy = 3 as Foo
const fooz: Foo = 3

const fooa = <3>3
const foob = <Foo>3

const fooc = 3
const bar = fooc!

function foo (x: number): number {
	return x! // unnecessary non-null
}
{
	"compilerOptions": { "strict": true },
	"include": [ "./code.ts" ],
}

Expected Result

3:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
4:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
7:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
8:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
11:13  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
14:9   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion

✖ 6 problems (6 errors, 0 warnings)

OR (see below)

3:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
4:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
5:7    error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
7:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
8:14   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
11:13  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
14:9   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion

✖ 7 problems (7 errors, 0 warnings)

Actual Result

  11:13  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
  14:9   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion

✖ 2 problems (2 errors, 0 warnings)

Additional Info

The typesToIgnore option description implies also checking annotations. Since annotations and assertions are different things, I would expect them to be handled by separate rules. Depending on whether or not they are an intended target for this rule, the first or second expectation should be fulfilled.

Versions

package version
@typescript-eslint/eslint-plugin 5.10.0
@typescript-eslint/parser 5.10.0
TypeScript 4.5.5
ESLint 8.7.0
node 14.17.5, 16.13.2
@c-vetter c-vetter added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Jan 24, 2022
@bradzacher
Copy link
Member

The rule specifically ignores as assertions to literal types.
This is because it's pretty common to do "useless" assertions on literals to prevent TS from narrowing the type.

For example:

let x = 1; // typeof x = number
let y = 1 as 1; // typeof y = 1

One could argue that you should use as const, but it's not always possible to use that - eg if you're not asserting on top of a literal:

const x = 1; // typeof x = 1
let y = x; // typeof y = number
let z1 = x as const; // ERROR -  'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals
let z2 = x as 1; // okay - typeof z2 = 1

So there are cases where a literal assertion is completely necessary, even though the thing it's asserting is already that type.

We could probably reduce the scope of the check i.e. ignore some cases rather than ignoring *all• cases. However the difficult thing with that is figuring out the cases that are always provably fine to ignore.
Which is where I would say that this is probably of dubious value, given the effort required to define and implement this set of cases.


Worth noting that the "literal-ness" of a type is resolved based on the type itself, regardless of if it's an alias - which is why Foo is considered a literal type, even though it's a type alias.

@bradzacher bradzacher added awaiting response Issues waiting for a reply from the OP or another party and removed triage Waiting for team members to take a look labels Jan 25, 2022
@c-vetter
Copy link
Author
c-vetter commented Feb 1, 2022

I get that ensuring no false positives and no false negatives is quite difficult. And if the most complex cases are not found, I think that's better than false positives.

However, the examples show straightforward cases, and they are lifted directly from the docs.

This is because it's pretty common to do "useless" assertions on literals to prevent TS from narrowing the type.

That would target this 👇 case, which is actually not useless (although I would suggest an annotation instead of an assertion):

const foo = 3 as number;

In the samples, we are generally asserting that 3 is of type 3, which is unquestionabley redundant. Can't the rule at least check for exact matches? That would already be helpful. Also, in my naive thinking, checking for an exact match should be only slightly more difficult than excluding literals – what am I missing?

One could argue that you should use as const

Actually, as const would be useless in these cases as well, the values being set in consts to begin with.


I added the “correct” samples, plus a line using the function at the end:

//…

const foon = <number>3;

const foom = 3 as number;

const fool = 'foo' as const;

function fook(x: number | undefined): number {
  return x!;
}

const foos = fook(3) as number

Now, I get this output:

  11:13  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
  14:9   error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
  27:14  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion

✖ 3 problems (3 errors, 0 warnings)

So, I can see that it works outside simple literal assignments.


All in all, I think at least a docs update is in order, and checking for exact matches with literals would be a nice addition.
This is how I see it:

doAsync()
.catch((error) => {
	console.error(error)
})

type Foo = 3

// bad
const foox: Foo = 3 as 3
// bad
const fooy = 3 as Foo
// bad ‑ probably a different rule since this is an annotation, not an assertion
const fooz: Foo = 3

// bad
const fooa = <3>3
// bad
const foob = <Foo>3

// good
const fooc = 3
// bad
const bar = fooc!

function foo (x: number): number {
	// bad
	return x!
}

// good
const foon = <number>3

// good
const foom = 3 as number

// bad
const fool = 'foo' as const

function fook(x: number | undefined): number {
	// good
	return x!
}

// bad
const foos = fook(3) as number

// good
let vy = 1 as 1
// bad
const cy = 1 as 1

const cx = 1
// bad
let z0 = cx as number
// bad, nice to check but not necessary
let z1 = cx as const
// good
let z2 = cx as 1

@bradzacher
Copy link
Member

That would target this 👇 case, which is actually not useless
const foo = 3 as number;

Note that this case is ignored intentionally because the type 3 and the type number are different types, not because of the literal rules.


There are two cases the rule ignores:

if (
isTypeFlagSet(castType, ts.TypeFlags.Literal) ||
(isObjectType(castType) &&
(isObjectFlagSet(castType, ts.ObjectFlags.Tuple) ||
couldBeTupleType(castType)))
) {
// It's not always safe to remove a cast to a literal type or tuple
// type, as those types are sometimes widened without the cast.
return;
}

  1. an assertion to a literal type (eg const x = y as 3).
  2. an assertion to a tuple type (eg const x = y as [number, string]).

It doesn't matter what the value is in the assertion - if the asserted type resolves to a type that matches one of the above, it is ignored.

AFAIK the tuple type case is never safe to remove, even in a const variable (as TS will widen the type from tuple to array). But there are cases where the literal restriction can be removed safely.

I'm happy for someone to teach the rule about cases where it is 100% provably safe to not apply the above cases.

@bradzacher bradzacher added accepting prs Go ahead, send a pull request that resolves this issue enhancement New feature or request and removed awaiting response Issues waiting for a reply from the OP or another party labels Feb 1, 2022
@lgenzelis
Copy link
Contributor

Hi! I've run into this issue too. I created a bare playground with no configuration at all, added the no-unnecessary-type-assertion rule, and copied the "incorrect code examples" from the documentation. As @c-vetter is saying, only the non-null-assertion part of the rule seems to work.

The rule specifically ignores as assertions to literal types.

If this is the case, @bradzacher , maybe the documentation is what needs fixing. I mean, the rule would then be right, it's just that the docs are wrong.

See playground

@Zamiell
Copy link
Contributor
Zamiell commented Sep 15, 2023

IMO, ignoring literals is arbitrary and potentially unsafe. Can I do a PR for a examineLiteral flag that defaults to true (i.e. preserves the existing behavior)?

edit - typoed my intentions for the flag

@JoshuaKGoldberg
Copy link
Member

How would that be beneficial once #4485 (comment) is implemented?

I'm happy for someone to teach the rule about cases where it is 100% provably safe to not apply the above cases.

@Zamiell
Copy link
Contributor
Zamiell commented Sep 15, 2023

I don't know what the algorithm would be to determine 100% provably safe cases, so I can't help improve the rule in that way. Until someone comes along who is smart enough to implement this, I think the rule could be improved by a examineLiteral option, which would empower people who want maximally strict ESLint rules by having safer code checking.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 26, 2024
@bradzacher bradzacher added the locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. label Apr 14, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepting prs Go ahead, send a pull request that resolves this issue enhancement New feature or request locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
5 participants
0