Skip to content
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

Type-asserting function call on variable initialized using this causes false implicit-any in VSCode #51661

Open
OxleyS opened this issue Nov 28, 2022 · 3 comments
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@OxleyS
Copy link

OxleyS commented Nov 28, 2022

Bug Report

🔎 Search Terms

asserts, this, vscode, language server, implicit any

🕗 Version & Regression Information

  • This changed between versions 4.6 and 4.7
  • This is also a problem on Nightly (5.0.0-dev.20221128)

This issue seems to be specific to VSCode's TS support - I have not been able to reproduce it in either the Playground nor the Bug Workbench. tsc itself accepts this code with no issues.

💻 Code

const assertExists: <T extends number>(actual: T | null | undefined) => asserts actual is T = () => {};

type Value = { n: number | undefined };
const callThisFunc = (f: (this: Value) => void): void => {
  f.call({ n: 42 });
};

callThisFunc(function() {
  const maybeN = this.n;
  assertExists(maybeN);
});

🙁 Actual behavior

Sometimes, usually when freshly loading this code, this works normally. If you edit the function body passed to callThisFunc in any way, even stuff like adding superfluous semicolons, maybeN will become red-underlined and complain about implicit-any on hover. Further appearances of maybeN in the function are also treated as any.

This error does not happen if you do not call assertExists, nor does it happen if you give this an explicit type, like so:

callThisFunc(function(this: Value) {
  const maybeN = this.n;
  assertExists(maybeN);
});

🙂 Expected behavior

This should not display any errors in VSCode, and the assertExists call should correctly narrow the type to number.

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this labels Dec 2, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Dec 2, 2022
@OxleyS
Copy link
Author

OxleyS commented Dec 21, 2022

I've been looking into this for a little bit. I'm very unfamiliar with the Typescript codebase so I'm somewhat flying blind, but from what I can tell, it seems that when resolving the type for maybeN, it computes the correct type for the this.n property access, but then runs into a circularity error, which smashes the type back out to any. Not sure why this is happening yet, but I'll keep digging if I can.

@OxleyS
Copy link
Author

OxleyS commented Dec 21, 2022

I digged a little further and found a source of circularity. I'm not sure if it's the only possible one, though. I'll put the full stack trace below in case it's any help but what I saw happening was this:

  1. Compiler wants to know the type of maybeN.
  2. To compute (1), we need to know the type of this.
  3. Function does not specify, so we try to infer it from the callThisFunc call.
  4. We check the one overload callThisFunc has for suitability.
  5. To compute suitability, we want more information about the argument to callThisFunc.
  6. We try to determine the return type of the function expression passed as an argument.
  7. To compute (6), we determine if the function has an implicit return.
  8. To compute (7), we see if the end flow node (the assertExists call) is reachable.
  9. To compute (8), we compute the signature of that call to see if it returns never.
  10. We try to infer the type of the argument to assertExists.
  11. We try to determine the type of maybeN. <--- CIRCULARITY HERE

Through this, I discovered that explicitly specifying the return type as void also seems to makes the problem go away. Anything that short-circuits the cycle, I guess.

I'm not yet sure why this is only happening in VSCode, but I also noticed that types of things seem to be resolved in a different order when the file is edited vs. loaded fresh. It's possible that the bug is dependent on that order difference.


Here's the full call stack on HEAD as I pulled it today (commit 54a554d8af2657630307cbfa8a3e4f3946e36507).

Call stack when the circularity was detected
pushTypeResolution (c:\dev\TypeScript\src\compiler\checker.ts:9803)
getTypeOfVariableOrParameterOrPropertyWorker (c:\dev\TypeScript\src\compiler\checker.ts:10861)
getTypeOfVariableOrParameterOrProperty (c:\dev\TypeScript\src\compiler\checker.ts:10799)
getTypeOfSymbol (c:\dev\TypeScript\src\compiler\checker.ts:11213)
getNarrowedTypeOfSymbol (c:\dev\TypeScript\src\compiler\checker.ts:27136)
checkIdentifier (c:\dev\TypeScript\src\compiler\checker.ts:27224)
checkExpressionWorker (c:\dev\TypeScript\src\compiler\checker.ts:36481)
checkExpression (c:\dev\TypeScript\src\compiler\checker.ts:36424)
checkExpressionWithContextualType (c:\dev\TypeScript\src\compiler\checker.ts:36029)
inferTypeArguments (c:\dev\TypeScript\src\compiler\checker.ts:31628)
chooseOverload (c:\dev\TypeScript\src\compiler\checker.ts:32445)
resolveCall (c:\dev\TypeScript\src\compiler\checker.ts:32279)
resolveCallExpression (c:\dev\TypeScript\src\compiler\checker.ts:32728)
resolveSignature (c:\dev\TypeScript\src\compiler\checker.ts:33185)
getResolvedSignature (c:\dev\TypeScript\src\compiler\checker.ts:33217)
getEffectsSignature (c:\dev\TypeScript\src\compiler\checker.ts:25587)
isReachableFlowNodeWorker (c:\dev\TypeScript\src\compiler\checker.ts:25646)
isReachableFlowNode (c:\dev\TypeScript\src\compiler\checker.ts:25615)
functionHasImplicitReturn (c:\dev\TypeScript\src\compiler\checker.ts:34392)
checkAndAggregateReturnExpressionTypes (c:\dev\TypeScript\src\compiler\checker.ts:34399)
getReturnTypeFromBody (c:\dev\TypeScript\src\compiler\checker.ts:34213)
contextuallyCheckFunctionExpressionOrObjectLiteralMethod (c:\dev\TypeScript\src\compiler\checker.ts:34578)
checkFunctionExpressionOrObjectLiteralMethod (c:\dev\TypeScript\src\compiler\checker.ts:34538)
checkExpressionWorker (c:\dev\TypeScript\src\compiler\checker.ts:36535)
checkExpression (c:\dev\TypeScript\src\compiler\checker.ts:36424)
checkExpressionWithContextualType (c:\dev\TypeScript\src\compiler\checker.ts:36029)
getSignatureApplicabilityError (c:\dev\TypeScript\src\compiler\checker.ts:31879)
chooseOverload (c:\dev\TypeScript\src\compiler\checker.ts:32418)
resolveCall (c:\dev\TypeScript\src\compiler\checker.ts:32279)
resolveCallExpression (c:\dev\TypeScript\src\compiler\checker.ts:32728)
resolveSignature (c:\dev\TypeScript\src\compiler\checker.ts:33185)
getResolvedSignature (c:\dev\TypeScript\src\compiler\checker.ts:33217)
getContextualTypeForArgumentAtIndex (c:\dev\TypeScript\src\compiler\checker.ts:28227)
getContextualTypeForArgument (c:\dev\TypeScript\src\compiler\checker.ts:28215)
getContextualType2 (c:\dev\TypeScript\src\compiler\checker.ts:28702)
getApparentTypeOfContextualType (c:\dev\TypeScript\src\compiler\checker.ts:28606)
getContextualSignature (c:\dev\TypeScript\src\compiler\checker.ts:29054)
getContextualThisParameterType (c:\dev\TypeScript\src\compiler\checker.ts:27954)
tryGetThisTypeAt (c:\dev\TypeScript\src\compiler\checker.ts:27600)
checkThisExpression (c:\dev\TypeScript\src\compiler\checker.ts:27562)
checkExpressionWorker (c:\dev\TypeScript\src\compiler\checker.ts:36485)
checkExpression (c:\dev\TypeScript\src\compiler\checker.ts:36424)
checkNonNullExpression (c:\dev\TypeScript\src\compiler\checker.ts:30366)
checkPropertyAccessExpression (c:\dev\TypeScript\src\compiler\checker.ts:30462)
checkExpressionWorker (c:\dev\TypeScript\src\compiler\checker.ts:36515)
checkExpression (c:\dev\TypeScript\src\compiler\checker.ts:36424)
checkExpressionCached (c:\dev\TypeScript\src\compiler\checker.ts:36059)
checkDeclarationInitializer (c:\dev\TypeScript\src\compiler\checker.ts:36082)
getTypeForVariableLikeDeclaration (c:\dev\TypeScript\src\compiler\checker.ts:10231)
getWidenedTypeForVariableLikeDeclaration (c:\dev\TypeScript\src\compiler\checker.ts:10730)
getTypeOfVariableOrParameterOrPropertyWorker (c:\dev\TypeScript\src\compiler\checker.ts:10914)
getTypeOfVariableOrParameterOrProperty (c:\dev\TypeScript\src\compiler\checker.ts:10799)
getTypeOfSymbol (c:\dev\TypeScript\src\compiler\checker.ts:11213)
getTypeOfNode (c:\dev\TypeScript\src\compiler\checker.ts:44221)
getTypeAtLocation (c:\dev\TypeScript\src\compiler\checker.ts:1540)
reclassifyByType (c:\dev\TypeScript\src\services\classifier2020.ts:224)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:155)
visitNode2 (c:\dev\TypeScript\src\compiler\parser.ts:427)
forEachChildInVariableDeclaration (c:\dev\TypeScript\src\compiler\parser.ts:544)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
visitNodes (c:\dev\TypeScript\src\compiler\parser.ts:436)
forEachChildInVariableDeclarationList (c:\dev\TypeScript\src\compiler\parser.ts:812)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
visitNode2 (c:\dev\TypeScript\src\compiler\parser.ts:427)
forEachChildInVariableStatement (c:\dev\TypeScript\src\compiler\parser.ts:809)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
visitNodes (c:\dev\TypeScript\src\compiler\parser.ts:436)
forEachChildInBlock (c:\dev\TypeScript\src\compiler\parser.ts:1160)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
visitNode2 (c:\dev\TypeScript\src\compiler\parser.ts:427)
forEachChildInFunctionExpression (c:\dev\TypeScript\src\compiler\parser.ts:637)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
visitNodes (c:\dev\TypeScript\src\compiler\parser.ts:436)
forEachChildInCallOrNewExpression (c:\dev\TypeScript\src\compiler\parser.ts:1156)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
visitNode2 (c:\dev\TypeScript\src\compiler\parser.ts:427)
forEachChildInExpressionStatement (c:\dev\TypeScript\src\compiler\parser.ts:815)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
visitNodes (c:\dev\TypeScript\src\compiler\parser.ts:436)
forEachChildInSourceFile (c:\dev\TypeScript\src\compiler\parser.ts:803)
forEachChild (c:\dev\TypeScript\src\compiler\parser.ts:1239)
visit (c:\dev\TypeScript\src\services\classifier2020.ts:188)
collectTokens (c:\dev\TypeScript\src\services\classifier2020.ts:192)
getSemanticTokens (c:\dev\TypeScript\src\services\classifier2020.ts:101)
getEncodedSemanticClassifications2 (c:\dev\TypeScript\src\services\classifier2020.ts:88)
getEncodedSemanticClassifications3 (c:\dev\TypeScript\src\services\services.ts:2310)
getEncodedSemanticClassifications (c:\dev\TypeScript\src\server\session.ts:1361)
encodedSemanticClassifications-full (c:\dev\TypeScript\src\server\session.ts:3321)
<anonymous> (c:\dev\TypeScript\src\server\session.ts:3534)
executeWithRequestId (c:\dev\TypeScript\src\server\session.ts:3524)
executeCommand (c:\dev\TypeScript\src\server\session.ts:3534)
onMessage (c:\dev\TypeScript\src\server\session.ts:3568)
<anonymous> (c:\dev\TypeScript\src\tsserver\nodeServer.ts:910)
emit (events:526)
emit (internal/child_process:938)
processTicksAndRejections (internal/process/task_queues:84)
TickObject (不明なソース:0)
init (internal/inspector_async_hook:25)
emitInitNative (internal/async_hooks:201)
emitInitScript (internal/async_hooks:506)
nextTick (internal/process/task_queues:133)
handleMessage (internal/child_process:954)
channel.onread (internal/child_process:623)
callbackTrampoline (internal/async_hooks:130)

@wlinna
Copy link

wlinna commented Oct 16, 2023

I have noticed this too with a slightly different setup (might be a different case). Here's a reproduction

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
    if (val == null) {
        throw new Error('val is nullish');
    }
}

interface Data {
    info?: number[] // Notice the question-mark
}

const data = {info: []} as Data;

const infoArr = data.info;
assertIsDefined(infoArr);

for (let i = 0; i < infoArr.length; i++) {
    const info = infoArr[i];
    assertIsDefined(info);
}

https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAQwM6oKYCcoElUAiGwMYGAJgDwAqAfABQBuyANgFyLUCUHamOqRMxaIYggHIJxIFi2QAjFhhq1EAbwBQibaOCImrRAF4jiMDJZd1WnbagALLHADuZjK4CiWJ1noByYVFBc1kxez8uAG4bbQBfDXiNUihsYGQIDEQCZChka1sAegLESVgMxAdMgEcQDFRYBEQAW2QsAGsAOkQY0TBgOAB+DnMm+WwAbQBdBI0NCAR6xHIcvNM1Un6OKdiUQWzc6LmFqF7+gEFvYyWVjo24aL5sPEJiUgp6O4usKNn+rH0lCcYFcAAyRUSISinOBfDpKMAAcwc4JgAGpUVZNLZ5mBFncrp9vOMYJNorZHjh8EQSGRyB8+nAfvEgA

Interestingly enough, the bug does not appear if I assert on data.info instead

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
    if (val == null) {
        throw new Error('val is nullish');
    }
}

interface Data {
    info?: number[]
}

const data = {info: []} as Data;

assertIsDefined(data.info);
const infoArr = data.info;

for (let i = 0; i < infoArr.length; i++) {
    const info = infoArr[i];
    assertIsDefined(info);
}

It doesn't happen either if I assign to data.info before assertIsDefined(infoArr);

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
    if (val == null) {
        throw new Error('val is nullish');
    }
}

interface Data {
    info?: number[]
}

const data = {info: []} as Data;
data.info = []

const infoArr = data.info;
assertIsDefined(infoArr);

for (let i = 0; i < infoArr.length; i++) {
    const info = infoArr[i];
    assertIsDefined(info);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

No branches or pull requests

3 participants