Microsoft/TypeScript

Weird behaviour with an "evolving any" and a non-null assertion operator

Open

#60,514 建立於 2024年11月16日

在 GitHub 查看
 (3 留言) (2 反應) (0 負責人)TypeScript (6,726 fork)batch import
BugDomain: check: Control FlowHelp Wanted

倉庫指標

Star
 (48,455 star)
PR 合併指標
 (平均合併 6天 17小時) (30 天內合併 9 個 PR)

描述

🔎 Search Terms

"evolving any", "non-null assertion", "compiler api"

🕗 Version & Regression Information

  • This changed in version 5.1

⏯ Playground Link

https://typescript-eslint.io/play/#ts=5.5.2&fileType=.tsx&code=CYUwxgNghgTiAEAzArgOzAFwJYHtVJxwAoBKALnlWQFsAjEGeAH3jVES1RGAFgAoUJFgIU6bHni1YpClToN%2B-UZlz5qUTgEZS8AN7948CCAzwAHgG4D5%2BAF5J0klb6GzdgsSf8A9N4B6APzWxqYAnpp21r6GgdaG8QmWUd4J8bEuRibwoQBMkRnR8OmpJWYAhM6GhanFmWEAzPlVKUVBGSXxblAAzpQ09DCV8NUJ6QC%2B-EA&eslintrc=N4KABGBEBOCuA2BTAzpAXGYBfEWg&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3QAacDUhFYASSSomyPAEEiATwphR6KQF8Q%2BoA&tokens=false

NOTE: I linked to the typescript-eslint playground just because it both supports // ^? and also provides access to the TS types so you can easily see the issue. You can ofc reproduce this in the TS playground too.

💻 Code

declare function foo(): number | undefined
declare function bar(): number

function main1() {
  let x;
  x = bar();
  x = foo();
//^?
  let y1 =
  //  ^?
           x;
  //       ^?
  let y2 =
  //  ^?
           x!;
  //       ^?
  let y3 =
  //  ^?
           x as number;
  //       ^?
}

🙁 Actual behavior

declare function foo(): number | undefined
declare function bar(): number

function main1() {
  let x;
  x = bar();
  x = foo();
//^? any ✅
  let y1 =
  //  ^? number | undefined ✅
           x;
  //       ^? number | undefined ✅
  let y2 =
  //  ^? number ✅
           x!;
  //       ^? number ❌
  let y3 =
  //  ^? number ✅
           x as number;
  //       ^? number | undefined ✅
}

🙂 Expected behavior

declare function foo(): number | undefined
declare function bar(): number

function main1() {
  let x;
  x = bar();
  x = foo();
//^? any ✅
  let y1 =
  //  ^? number | undefined ✅
           x;
  //       ^? number | undefined ✅
  let y2 =
  //  ^? number ✅
           x!;
  //       ^? number | undefined ❓❓❓❓
  let y3 =
  //  ^? number ✅
           x as number;
  //       ^? number | undefined ✅
}

Additional information about the issue

Reference: https://github.com/typescript-eslint/typescript-eslint/issues/10334

TypeScript-ESLint has a rule called no-unnecessary-type-assertion which, as the name implies, flags unnecessary type assertions to help keep the code clean.

One of the checks done by the rule involves flagging unnecessary non-null assertions. The basic logic for this check is "if the type of the thing does not include null | undefined then the non-null assertion is unnecessary". By "the thing" I am referring to the expression being non-null asserted, eg the x in x!.

In the case of an "evolving any" the type of the variable is shown to be incorrect by the TS APIs. As shown in the example code (specifically the y2 case) - the type of x is presented as number - even though it should be presented as number | undefined. It appears that for some reason the non-null assertion modifies the type of the variable to remove the | undefined, rather than just emitting a non-nullish type from the non-null assertion expression.

This is made even weirder from the fact that writing x as number behaves correctly and the type of x is correctly presented as number | undefined.

This discrepancy is problematic for our lint rule because it means we currently false-positive on this case because when we query for the type of x it looks non-nullish -- which suggests the non-null assertion is unnecessary. But if the user removes the non-null assertion then the type of x changes to number | undefined and this causes errors.

貢獻者指南