Repository: dsherret/ts-morph (2025-11-13T04:45:58Z)

Issue:
CallExpression.addTypeArgument crashes if the CallExpression's expression is a PropertyAccessExpression
**Describe the bug**

Version: 13.0.2

addTypeArgument() only seems to work for simple call expressions of the form `foo()`, but not forms like `this.foo()`.

**To Reproduce**

Consider this source file:

```
function foo<T>(_param: T): void {
}

export class Test {
    public foo<T>(_param: T): void {
    }

    public bar(): void {
        this.foo({});
        new Test().foo({});
        foo({});
    }
}
```
And this program:

```
import * as AST from "ts-morph";

const project = new AST.Project();
const sourceFile = project.addSourceFileAtPath("./CallExprAddTypeArgument.ts");
sourceFile.getDescendantsOfKind(AST.SyntaxKind.CallExpression).forEach(callExpr => {
    callExpr.addTypeArgument("object");
});
```

We see the following crash for `this.foo({})` and `new Test().foo({})`, while it works as expected for `foo({})`

```
.../node_modules/@ts-morph/common/dist/ts-morph-common.js:473
            throw new InvalidOperationError(typeof errorMessage === "string" ? errorMessage : errorMessage());
            ^

Error: A child of the kind Identifier was expected.
    at Object.throwIfNullOrUndefined (.../node_modules/@ts-morph/common/dist/ts-morph-common.js:473:19)
    at CallExpression.getFirstChildByKindOrThrow (.../node_modules/ts-morph/dist/ts-morph.js:3754:30)
    at CallExpression.insertTypeArguments (.../node_modules/ts-morph/dist/ts-morph.js:10006:41)
    at CallExpression.addTypeArguments (.../node_modules/ts-morph/dist/ts-morph.js:9995:25)
    at CallExpression.addTypeArgument (.../node_modules/ts-morph/dist/ts-morph.js:9992:25)
    ...
```

It appears as though insertTypeArguments assumes it will have an identifier as a direct child. In reality, it could be an expressioned node and thus the identifier is not a direct child.

**Expected behavior**

The type argument should be added without crashing for all three of the example call expressions.




Discussion:
From some quick testing, I believe something like this in `TypeArgumentedNode.ts` addresses the issue, though I'm unsure if there are any more complicating factors.

```
    insertTypeArguments(index: number, argumentTexts: ReadonlyArray<string>) {
    ...
      if (typeArguments.length === 0) {
        const expression = Node.hasExpression(this) ? this.getExpression() : undefined;
        const nodeWithIdentifier = Node.isPropertyAccessExpression(expression) ? expression : this;
        const identifier = nodeWithIdentifier.getFirstChildByKindOrThrow(SyntaxKind.Identifier);
        insertIntoParentTextRange({
        ...
```
I experience the same issue. Have you come around this?



Patch:
diff --git a/packages/ts-morph/src/compiler/ast/base/TypeArgumentedNode.ts b/packages/ts-morph/src/compiler/ast/base/TypeArgumentedNode.ts
index 4c60e16f6..60e5a0b2c 100644
--- a/packages/ts-morph/src/compiler/ast/base/TypeArgumentedNode.ts
+++ b/packages/ts-morph/src/compiler/ast/base/TypeArgumentedNode.ts
@@ -86,7 +86,12 @@ export function TypeArgumentedNode<T extends Constructor<TypeArgumentedNodeExten
       index = verifyAndGetIndex(index, typeArguments.length);
 
       if (typeArguments.length === 0) {
-        const identifier = this.getFirstChildByKindOrThrow(SyntaxKind.Identifier);
+        // For property access expressions like `this.foo()`, the identifier is nested
+        // inside the PropertyAccessExpression, not a direct child of the CallExpression
+        const expression = Node.hasExpression(this) ? this.getExpression() : undefined;
+        const identifier = Node.isPropertyAccessExpression(expression)
+          ? expression.getLastChildByKindOrThrow(SyntaxKind.Identifier)
+          : this.getFirstChildByKindOrThrow(SyntaxKind.Identifier);
         insertIntoParentTextRange({
           insertPos: identifier.getEnd(),
           parent: this,


Test patch:
diff --git a/packages/ts-morph/src/tests/compiler/ast/base/typeArgumentedNodeTests.ts b/packages/ts-morph/src/tests/compiler/ast/base/typeArgumentedNodeTests.ts
index 52ea8db14..cba8e1a33 100644
--- a/packages/ts-morph/src/tests/compiler/ast/base/typeArgumentedNodeTests.ts
+++ b/packages/ts-morph/src/tests/compiler/ast/base/typeArgumentedNodeTests.ts
@@ -1,6 +1,6 @@
-import { nameof } from "@ts-morph/common";
+import { nameof, SyntaxKind } from "@ts-morph/common";
 import { expect } from "chai";
-import { ClassDeclaration, TypeArgumentedNode } from "../../../../compiler";
+import { CallExpression, ClassDeclaration, TypeArgumentedNode } from "../../../../compiler";
 import { getInfoFromText } from "../../testHelpers";
 
 describe("TypeArgumentedNode", () => {
@@ -86,6 +86,29 @@ describe("TypeArgumentedNode", () => {
     it("should add a type arg", () => {
       doTest("@dec<1, 2>()\nclass T {}", "3", "@dec<1, 2, 3>()\nclass T {}");
     });
+
+    // Issue #1228: addTypeArgument crashes on property access expressions
+    describe("property access expressions", () => {
+      function doTestPropertyAccess(code: string, text: string, expectedCode: string) {
+        const { sourceFile } = getInfoFromText(code);
+        const callExpr = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)[0];
+        const result = callExpr.addTypeArgument(text);
+        expect(result.getText()).to.equal(text);
+        expect(sourceFile.getFullText()).to.equal(expectedCode);
+      }
+
+      it("should add type argument to property access on this", () => {
+        doTestPropertyAccess("this.foo();", "string", "this.foo<string>();");
+      });
+
+      it("should add type argument to property access on constructor", () => {
+        doTestPropertyAccess("new Test().foo();", "string", "new Test().foo<string>();");
+      });
+
+      it("should add type argument to property access on object", () => {
+        doTestPropertyAccess("obj.method();", "string", "obj.method<string>();");
+      });
+    });
   });
 
   describe(nameof<TypeArgumentedNode>("removeTypeArgument"), () => {