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

Improve error position/range for arrow functions with expression body #57866

Open
6 tasks done
OliverJAsh opened this issue Mar 20, 2024 · 8 comments · May be fixed by #60799 or #60809
Open
6 tasks done

Improve error position/range for arrow functions with expression body #57866

OliverJAsh opened this issue Mar 20, 2024 · 8 comments · May be fixed by #60799 or #60809
Labels
Domain: Error Messages The issue relates to error messaging Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Mar 20, 2024

🔍 Search Terms

  • arrow function
  • return type
  • type error position/range

✅ Viability Checklist

⭐ Suggestion

For arrow functions with an expression body, the position of the return type error could be adjusted so it doesn't cover the entire expression. This would make it easier to identify more meaningful type errors that exist within the expression.

📃 Motivating Example

Consider the following example where we're using the pipe function inside arrow functions and the add function is missing.

declare const pipe: <A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C) => C;

// declare const add: (x: number) => (n: number) => number;
declare const identity: <T>(x: T) => T;

// Arrow function with block body
const f = (): number => {
  return pipe(1, add(1), identity);
};

// Arrow function with expression body
const g = (): number => pipe(1, add(1), identity);

Both of these arrow functions have two type errors:

  • For the return value: Type 'unknown' is not assignable to type 'number'.
  • Cannot find name 'add'.

The first type error can be fixed by addressing the second type error.

When the arrow function has a block body, these two error messages are highlighted separately:

image

However, when the arrow function has an expression body, the range of the return type error covers the entire expression:

image

This makes it very difficult to spot the inner type error for Cannot find name 'add', especially in more advanced examples.

To help with this I was wondering if we could move the position of the return type so it doesn't cover the entire expression. Perhaps it could be positioned on the => that appears immediately before the expression? This would be closer to the behaviour of arrow functions with body blocks.

Note this issue does not occur when we're not using the pipe function. I believe this is because TypeScript treats the return type of add(1) as any, whereas with pipe the type argument B will be inferred as unknown (which is desired in other cases).

const g2 = (): number => identity(add(1)(1));
image

💻 Use Cases

See above.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 20, 2024

Seems reasonable. If a return type annotation exists, we can put it there

// Arrow function with expression body
// Today
const g1: () => number = () => pipe(1, add(1), identity);
//                             ~~~~~~~~~~~~~~~~~~~~~~~~~
// Proposed
const g2: () => number = () => pipe(1, add(1), identity);
//                          ~~
// Or (?)
const g3 = (): number => pipe(1, add(1), identity);
//             ~~~~~~

Aside: Try to guess where the error span is in this code! 🫠

const p: () => string = (): number => 32;

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Experience Enhancement Noncontroversial enhancements Help Wanted You can do this labels Mar 20, 2024
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Mar 20, 2024
@DanielRosenwasser DanielRosenwasser added the Domain: Error Messages The issue relates to error messaging label Mar 20, 2024
@DanielRosenwasser
Copy link
Member

I kind of like moving it to the return type itself, but either way I feel like we should adjust the error message in these cases if possible. Something like

This function returns the type 'unknown' which is not assignable to its declared type 'number'.

@fatcerberus
Copy link

I recall that I once suggested putting the error span on the => for this exact reason, by analogy to the return in the block function. IIRC @Andarist objected.

@Andarist
Copy link
Contributor

The mentioned discussion happened here. I don't mind this change if the team likes it. My main problem with it is that it's a pretty short error span but OTOH you can end up with even single-character error spans anyway so that concern can be ignored.

It should be OK - especially with the adjusted error message like the one proposed by @DanielRosenwasser .

I'm pretty sure I'll this change, as I've also mentioned in that same comment 😉 :

I prefer to highlight smaller spans over huge ones

@OliverJAsh
Copy link
Contributor Author

I would like to raise a PR for this change because I think it will significantly improve DX. Can anyone guide me to where in the code I need to make the change? Hopefully I can figure it out from there.

@Andarist
Copy link
Contributor

checkFunctionExpressionOrObjectLiteralMethodDeferred passed down effectiveCheckNode to checkTypeAssignableToAndOptionallyElaborate both as the checked expression and the error node. The error node reaches getErrorSpanForNode and that's where the error span gets computed.

One way to fix this would be to pass node (aka the whole arrow function) as errorNode to checkTypeAssignableToAndOptionallyElaborate and then tweak the logic in getErrorSpanForArrowFunction. As it is, this wouldn't fix the issue because getErrorSpanForArrowFunction highlights the first line... but in your case that's the whole thing ;p

@OliverJAsh
Copy link
Contributor Author

OliverJAsh commented Dec 17, 2024

Thanks @Andarist but I think there may be a misunderstanding. If I'm not mistaken, getErrorSpanForArrowFunction describes error spans for arrow functions as a whole, whereas what I'm interested in here is just the span of the return expression.

I just found this PR (for #52934) of yours which fixed the issue for arrow functions with block bodies. All I want to do here is make the same change for arrow functions with expression (non-block) bodies.

I've got a WIP PR here, but I might need some more guidance.

OliverJAsh added a commit to OliverJAsh/TypeScript that referenced this issue Dec 17, 2024
Fixes microsoft#57866
diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts
index 1ff3a2acad..2ea2c5ee56 100644
--- a/src/compiler/utilities.ts
+++ b/src/compiler/utilities.ts
@@ -2527,6 +2527,14 @@ export function getErrorSpanForNode(sourceFile: SourceFile, node: Node): TextSpa
         }
     }

+    if (node.parent !== undefined && node.parent.kind === SyntaxKind.ArrowFunction) {
+        const arrowFn = node.parent as ArrowFunction;
+        if (arrowFn.body === node && arrowFn.type !== undefined) {
+            const pos = skipTrivia(sourceFile.text, arrowFn.type.pos);
+            return getSpanOfTokenAtPosition(sourceFile, pos);
+        }
+    }
+
     if (errorNode === undefined) {
         // If we don't have a better node, then just set the error on the first token of
         // construct.
diff --git a/tests/baselines/reference/conditionalTypes1.errors.txt b/tests/baselines/reference/conditionalTypes1.errors.txt
index aa2ae24033..476345ed9f 100644
--- a/tests/baselines/reference/conditionalTypes1.errors.txt
+++ b/tests/baselines/reference/conditionalTypes1.errors.txt
@@ -73,7 +73,7 @@ conditionalTypes1.ts(160,5): error TS2322: Type 'T' is not assignable to type 'Z
   Type 'string | number' is not assignable to type 'ZeroOf<T>'.
     Type 'string' is not assignable to type 'ZeroOf<T>'.
 conditionalTypes1.ts(263,9): error TS2403: Subsequent variable declarations must have the same type.  Variable 'z' must be of type 'T1', but here has type 'Foo<T & U>'.
-conditionalTypes1.ts(288,43): error TS2322: Type 'T95<U>' is not assignable to type 'T94<U>'.
+conditionalTypes1.ts(288,33): error TS2322: Type 'T95<U>' is not assignable to type 'T94<U>'.
   Type 'number | boolean' is not assignable to type 'T94<U>'.
     Type 'number' is not assignable to type 'T94<U>'.
       Type 'boolean' is not assignable to type 'true'.
@@ -465,7 +465,7 @@ conditionalTypes1.ts(288,43): error TS2322: Type 'T95<U>' is not assignable to t
     type T95<T> = T extends string ? boolean : number;
     const f44 = <U>(value: T94<U>): T95<U> => value;
     const f45 = <U>(value: T95<U>): T94<U> => value;  // Error
-                                              ~~~~~
+                                    ~~~
 !!! error TS2322: Type 'T95<U>' is not assignable to type 'T94<U>'.
 !!! error TS2322:   Type 'number | boolean' is not assignable to type 'T94<U>'.
 !!! error TS2322:     Type 'number' is not assignable to type 'T94<U>'.
diff --git a/tests/baselines/reference/mappedTypeConstraints2.errors.txt b/tests/baselines/reference/mappedTypeConstraints2.errors.txt
index 0d3b3244c9..098bbd3f4b 100644
--- a/tests/baselines/reference/mappedTypeConstraints2.errors.txt
+++ b/tests/baselines/reference/mappedTypeConstraints2.errors.txt
@@ -6,7 +6,7 @@ mappedTypeConstraints2.ts(16,11): error TS2322: Type 'Mapped3<K>[Uppercase<K>]'
 mappedTypeConstraints2.ts(42,7): error TS2322: Type 'Mapped6<K>[keyof Mapped6<K>]' is not assignable to type '`_${string}`'.
   Type 'Mapped6<K>[string] | Mapped6<K>[number] | Mapped6<K>[symbol]' is not assignable to type '`_${string}`'.
     Type 'Mapped6<K>[string]' is not assignable to type '`_${string}`'.
-mappedTypeConstraints2.ts(51,57): error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
+mappedTypeConstraints2.ts(51,52): error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
   'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.
 mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K>[`_${K}`]' is not assignable to type 'true'.
   Type 'ObjectWithUnderscoredKeys<K>[`_${string}`]' is not assignable to type 'true'.
@@ -75,7 +75,7 @@ mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K
     };

     const get = <T extends string>(t: T, foo: Foo<T>): T => foo[`get${t}`];  // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
-                                                            ~~~~~~~~~~~~~~
+                                                       ~
 !!! error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
 !!! error TS2322:   'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.

diff --git a/tests/baselines/reference/parserArrowFunctionExpression17.errors.txt b/tests/baselines/reference/parserArrowFunctionExpression17.errors.txt
index 28091b1745..73c2b637f0 100644
--- a/tests/baselines/reference/parserArrowFunctionExpression17.errors.txt
+++ b/tests/baselines/reference/parserArrowFunctionExpression17.errors.txt
@@ -1,12 +1,12 @@
 fileJs.js(1,1): error TS2304: Cannot find name 'a'.
 fileJs.js(1,5): error TS2304: Cannot find name 'b'.
 fileJs.js(1,15): error TS2304: Cannot find name 'd'.
+fileJs.js(1,15): error TS2304: Cannot find name 'e'.
 fileJs.js(1,15): error TS8010: Type annotations can only be used in TypeScript files.
-fileJs.js(1,20): error TS2304: Cannot find name 'e'.
 fileTs.ts(1,1): error TS2304: Cannot find name 'a'.
 fileTs.ts(1,5): error TS2304: Cannot find name 'b'.
 fileTs.ts(1,15): error TS2304: Cannot find name 'd'.
-fileTs.ts(1,20): error TS2304: Cannot find name 'e'.
+fileTs.ts(1,15): error TS2304: Cannot find name 'e'.

 ==== fileJs.js (5 errors) ====
@@ -18,9 +18,9 @@ fileTs.ts(1,20): error TS2304: Cannot find name 'e'.
                   ~
 !!! error TS2304: Cannot find name 'd'.
                   ~
-!!! error TS8010: Type annotations can only be used in TypeScript files.
-                       ~
 !!! error TS2304: Cannot find name 'e'.
+                  ~
+!!! error TS8010: Type annotations can only be used in TypeScript files.

 ==== fileTs.ts (4 errors) ====
     a ? b : (c) : d => e
@@ -30,6 +30,6 @@ fileTs.ts(1,20): error TS2304: Cannot find name 'e'.
 !!! error TS2304: Cannot find name 'b'.
                   ~
 !!! error TS2304: Cannot find name 'd'.
-                       ~
+                  ~
 !!! error TS2304: Cannot find name 'e'.

\ No newline at end of file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Error Messages The issue relates to error messaging Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
5 participants