Repository: microsoft/typescript-go (go)

Issue:
`tsgo` typechecking error in tsx code that passes when using `tsc`
I'm trying out tsgo on my large work codebase and found it errors on a pattern we use very often that does pass `tsc`:

```ts
import type { ComponentType, JSX } from "react"

export type Page<P = NonNullable<unknown>> = ComponentType<P> & {
    getLayout?: (component: JSX.Element) => JSX.Element
}

export const FooPage: Page = () => {
    return (
        <div>
            <p>Foo</p>
        </div>
    )
}

// `tsgo` errors here: "'getLayout' implicitly has type 'any' because it does not have a type annotation
//                                  and is referenced directly or indirectly in its own initializer."
FooPage.getLayout = () => {
    return <></>
}
```

(_And on importing vite's `defineConfig`, but I see someone else beat me to that in #904._)

I was able to reproduce my error in a much more barebones project: https://github.com/justinsmid/tsgo-test
I'm sure the `tsconfig.json` could be simplified, and I'm not sure if it's related to any setting in there, but I wanted to keep the config as close to that of my work repo as I could.

Running `pnpm typecheck` (which uses `tsc`) passes, but `pnpm typecheck-go` does not:

```
> tsgo --noEmit

src/components/Foo.tsx:15:1 - error TS7022: 'getLayout' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

15 FooPage.getLayout = () => {
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16     return <></>
   ~~~~~~~~~~~~~~~~
17 }
   ~
vite.config.ts:1:10 - error TS2305: Module '"[REDACTED]/tsgo-test/node_modules/.pnpm/vite@6.3.5/node_modules/vite/index"' has no exported member 'defineConfig'.

1 import { defineConfig } from 'vite';
           ~~~~~~~~~~~~

Found 2 errors in 2 files.

Errors  Files
     1  src/components/Foo.tsx:15
     1  vite.config.ts:1

 ELIFECYCLE  Command failed with exit code 2.
```

Pull request:
Non-circular type resolution for expando assignment declarations
Fixes #921.

Patch:
diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go
index cdc8d6e9e..a47b4a00c 100644
--- a/cmd/tsgo/lsp.go
+++ b/cmd/tsgo/lsp.go
@@ -1,10 +1,8 @@
 package main
 
 import (
-	"errors"
 	"flag"
 	"fmt"
-	"io"
 	"os"
 
 	"github.com/microsoft/typescript-go/internal/bundled"
@@ -49,7 +47,7 @@ func runLSP(args []string) int {
 		DefaultLibraryPath: defaultLibraryPath,
 	})
 
-	if err := s.Run(); err != nil && !errors.Is(err, io.EOF) {
+	if err := s.Run(); err != nil {
 		return 1
 	}
 	return 0
diff --git a/internal/checker/checker.go b/internal/checker/checker.go
index 87168d9b0..3925aea5c 100644
--- a/internal/checker/checker.go
+++ b/internal/checker/checker.go
@@ -27686,7 +27686,10 @@ func (c *Checker) getContextualTypeForBinaryOperand(node *ast.Node, contextFlags
 	case ast.KindEqualsToken, ast.KindAmpersandAmpersandEqualsToken, ast.KindBarBarEqualsToken, ast.KindQuestionQuestionEqualsToken:
 		// In an assignment expression, the right operand is contextually typed by the type of the left operand
 		// unless it's an assignment declaration.
-		if node == binary.Right && !c.isReferenceToModuleExports(binary.Left) && (binary.Symbol == nil || c.canGetContextualTypeForAssignmentDeclaration(node.Parent)) {
+		if node == binary.Right && !c.isReferenceToModuleExports(binary.Left) {
+			if binary.Symbol != nil {
+				return c.getContextualTypeForAssignmentDeclaration(node.Parent)
+			}
 			return c.getTypeOfExpression(binary.Left)
 		}
 	case ast.KindBarBarToken, ast.KindQuestionQuestionToken:
@@ -27708,14 +27711,15 @@ func (c *Checker) getContextualTypeForBinaryOperand(node *ast.Node, contextFlags
 	return nil
 }
 
-func (c *Checker) canGetContextualTypeForAssignmentDeclaration(node *ast.Node) bool {
+func (c *Checker) getContextualTypeForAssignmentDeclaration(node *ast.Node) *Type {
 	// Node is the left operand of an assignment declaration (a binary expression with a symbol assigned by the
 	// binder) of the form 'F.id = expr' or 'F[xxx] = expr'. If 'F' is declared as a variable with a type annotation,
 	// we can obtain a contextual type from the annotated type without triggering a circularity. Otherwise, the
 	// assignment declaration has no contextual type.
 	left := node.AsBinaryExpression().Left
 	expr := left.Expression()
-	if ast.IsAccessExpression(left) && expr.Kind == ast.KindThisKeyword {
+	switch expr.Kind {
+	case ast.KindThisKeyword:
 		var symbol *ast.Symbol
 		if ast.IsPropertyAccessExpression(left) {
 			name := left.Name()
@@ -27734,27 +27738,39 @@ func (c *Checker) canGetContextualTypeForAssignmentDeclaration(node *ast.Node) b
 		if symbol != nil {
 			d := symbol.ValueDeclaration
 			if d != nil && (ast.IsPropertyDeclaration(d) || ast.IsPropertySignatureDeclaration(d)) && d.Type() == nil && d.Initializer() == nil {
-				return false
+				return nil
 			}
 		}
 		symbol = node.Symbol()
 		if symbol != nil && symbol.ValueDeclaration != nil && symbol.ValueDeclaration.Type() == nil {
 			if !ast.IsObjectLiteralMethod(c.getThisContainer(expr, false, false)) {
-				return false
+				return nil
 			}
 			// and now for one single case of object literal methods
 			name := ast.GetElementOrPropertyAccessArgumentExpressionOrName(left)
 			if name == nil {
-				return false
+				return nil
 			} else {
 				// !!! contextual typing for `this` in object literals
-				return false
+				return nil
+			}
+		}
+		return c.getTypeOfExpression(left)
+	case ast.KindIdentifier:
+		symbol := c.getExportSymbolOfValueSymbolIfExported(c.getResolvedSymbol(expr))
+		if symbol.ValueDeclaration != nil && ast.IsVariableDeclaration(symbol.ValueDeclaration) {
+			if typeNode := symbol.ValueDeclaration.Type(); typeNode != nil {
+				if ast.IsPropertyAccessExpression(left) {
+					return c.getTypeOfPropertyOfContextualType(c.getTypeFromTypeNode(typeNode), left.Name().Text())
+				}
+				nameType := c.checkExpressionCached(left.AsElementAccessExpression().ArgumentExpression)
+				if isTypeUsableAsPropertyName(nameType) {
+					return c.getTypeOfPropertyOfContextualTypeEx(c.getTypeFromTypeNode(typeNode), getPropertyNameFromType(nameType), nameType)
+				}
 			}
 		}
-		return true
 	}
-	symbol := c.getExportSymbolOfValueSymbolIfExported(c.getResolvedSymbol(expr))
-	return symbol.ValueDeclaration != nil && ast.IsVariableDeclaration(symbol.ValueDeclaration) && symbol.ValueDeclaration.Type() != nil
+	return nil
 }
 
 func (c *Checker) isReferenceToModuleExports(node *ast.Node) bool {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index b9d935329..77d482424 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -187,16 +187,17 @@ func (s *Server) Run() error {
 	g.Go(func() error { return s.dispatchLoop(ctx) })
 	g.Go(func() error { return s.writeLoop(ctx) })
 	g.Go(func() error { return s.readLoop(ctx) })
-	return g.Wait()
+
+	if err := g.Wait(); err != nil && !errors.Is(err, io.EOF) {
+		return err
+	}
+	return nil
 }
 
 func (s *Server) readLoop(ctx context.Context) error {
 	for {
 		msg, err := s.read()
 		if err != nil {
-			if errors.Is(err, io.EOF) {
-				return nil
-			}
 			if errors.Is(err, lsproto.ErrInvalidRequest) {
 				s.sendError(nil, err)
 				continue


Test patch:
diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go
index 3f4474789..9c02222fb 100644
--- a/internal/testutil/harnessutil/harnessutil.go
+++ b/internal/testutil/harnessutil/harnessutil.go
@@ -146,6 +146,10 @@ func CompileFilesEx(
 		}
 	}
 
+	if includeLibDir {
+		repo.SkipIfNoTypeScriptSubmodule(t)
+	}
+
 	// !!!
 	// ts.assign(options, ts.convertToOptionsWithAbsolutePaths(options, path => ts.getNormalizedAbsolutePath(path, currentDirectory)));
 	if compilerOptions.OutDir != "" {
diff --git a/testdata/baselines/reference/compiler/expandoContextualTypes.symbols b/testdata/baselines/reference/compiler/expandoContextualTypes.symbols
new file mode 100644
index 000000000..42700f77e
--- /dev/null
+++ b/testdata/baselines/reference/compiler/expandoContextualTypes.symbols
@@ -0,0 +1,52 @@
+//// [tests/cases/compiler/expandoContextualTypes.tsx] ////
+
+=== expandoContextualTypes.tsx ===
+// https://github.com/microsoft/typescript-go/issues/921
+
+/// <reference path="react16.d.ts" />
+
+import type { ComponentType } from "react";
+>ComponentType : Symbol(ComponentType, Decl(expandoContextualTypes.tsx, 4, 13))
+
+export type Page<P = NonNullable<unknown>> = ComponentType<P> & {
+>Page : Symbol(Page, Decl(expandoContextualTypes.tsx, 4, 43))
+>P : Symbol(P, Decl(expandoContextualTypes.tsx, 6, 17))
+>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --))
+>ComponentType : Symbol(ComponentType, Decl(expandoContextualTypes.tsx, 4, 13))
+>P : Symbol(P, Decl(expandoContextualTypes.tsx, 6, 17))
+
+    getLayout?: (component: JSX.Element) => JSX.Element;
+>getLayout : Symbol(getLayout, Decl(expandoContextualTypes.tsx, 6, 65))
+>component : Symbol(component, Decl(expandoContextualTypes.tsx, 7, 17))
+>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12))
+>Element : Symbol(Element, Decl(react16.d.ts, 2494, 23))
+>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12))
+>Element : Symbol(Element, Decl(react16.d.ts, 2494, 23))
+}
+
+export const FooPage: Page = () => {
+>FooPage : Symbol(FooPage, Decl(expandoContextualTypes.tsx, 10, 12))
+>Page : Symbol(Page, Decl(expandoContextualTypes.tsx, 4, 43))
+
+    return (
+        <div>
+>div : Symbol(div, Decl(react16.d.ts, 2546, 114))
+
+            <p>Foo</p>
+>p : Symbol(p, Decl(react16.d.ts, 2593, 102))
+>p : Symbol(p, Decl(react16.d.ts, 2593, 102))
+
+        </div>
+>div : Symbol(div, Decl(react16.d.ts, 2546, 114))
+
+    )
+};
+
+FooPage.getLayout = () => {
+>FooPage.getLayout : Symbol(getLayout, Decl(expandoContextualTypes.tsx, 6, 65))
+>FooPage : Symbol(FooPage, Decl(expandoContextualTypes.tsx, 10, 12))
+>getLayout : Symbol(getLayout, Decl(expandoContextualTypes.tsx, 6, 65))
+
+    return <></>
+};
+
diff --git a/testdata/baselines/reference/compiler/expandoContextualTypes.types b/testdata/baselines/reference/compiler/expandoContextualTypes.types
new file mode 100644
index 000000000..bbf6d9844
--- /dev/null
+++ b/testdata/baselines/reference/compiler/expandoContextualTypes.types
@@ -0,0 +1,54 @@
+//// [tests/cases/compiler/expandoContextualTypes.tsx] ////
+
+=== expandoContextualTypes.tsx ===
+// https://github.com/microsoft/typescript-go/issues/921
+
+/// <reference path="react16.d.ts" />
+
+import type { ComponentType } from "react";
+>ComponentType : ComponentType<P>
+
+export type Page<P = NonNullable<unknown>> = ComponentType<P> & {
+>Page : Page<P>
+
+    getLayout?: (component: JSX.Element) => JSX.Element;
+>getLayout : ((component: Element) => Element) | undefined
+>component : Element
+>JSX : any
+>JSX : any
+}
+
+export const FooPage: Page = () => {
+>FooPage : Page<{}>
+>() => {    return (        <div>            <p>Foo</p>        </div>    )} : { (): Element; getLayout: () => Element; }
+
+    return (
+>(        <div>            <p>Foo</p>        </div>    ) : Element
+
+        <div>
+><div>            <p>Foo</p>        </div> : Element
+>div : any
+
+            <p>Foo</p>
+><p>Foo</p> : Element
+>p : any
+>p : any
+
+        </div>
+>div : any
+
+    )
+};
+
+FooPage.getLayout = () => {
+>FooPage.getLayout = () => {    return <></>} : () => Element
+>FooPage.getLayout : ((component: Element) => Element) | undefined
+>FooPage : StatelessComponent<{}> & { getLayout?: ((component: Element) => Element) | undefined; }
+>getLayout : ((component: Element) => Element) | undefined
+>() => {    return <></>} : () => Element
+
+    return <></>
+><></> : Element
+
+};
+
diff --git a/testdata/tests/cases/compiler/expandoContextualTypes.tsx b/testdata/tests/cases/compiler/expandoContextualTypes.tsx
new file mode 100644
index 000000000..ef6e65dff
--- /dev/null
+++ b/testdata/tests/cases/compiler/expandoContextualTypes.tsx
@@ -0,0 +1,26 @@
+// @strict: true
+// @target: esnext
+// @noEmit: true
+// @jsx: preserve
+
+// https://github.com/microsoft/typescript-go/issues/921
+
+/// <reference path="/.lib/react16.d.ts" />
+
+import type { ComponentType } from "react";
+
+export type Page<P = NonNullable<unknown>> = ComponentType<P> & {
+    getLayout?: (component: JSX.Element) => JSX.Element;
+}
+
+export const FooPage: Page = () => {
+    return (
+        <div>
+            <p>Foo</p>
+        </div>
+    )
+};
+
+FooPage.getLayout = () => {
+    return <></>
+};