microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.12k stars 12.29k forks source link

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

Open OliverJAsh opened 3 months ago

OliverJAsh commented 3 months ago

πŸ” Search Terms

βœ… 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:

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 commented 3 months ago

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;
DanielRosenwasser commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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