-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Description
Search Terms
TS2657 TS2695 TS2304 JSX map label:"Domain: JSX/TSX"
Suggestion
TS should emit clearer error messages for invalid JSX caused by the case where a developer expected code to be in a JSX expression context but instead it's evaluated in a non-JSX context (regular code). A signpost for these kinds of issues is that the compiler errors go away if the offending code is wrapped in a Fragment.
Use Cases
TS emits a helpful error message ([ts] JSX expressions must have one parent element. [2657]
) when a simple JSX expression has two sibling elements without a single parent element or fragment. But this only works if your code successfully emits multiple JSX elements. If you're combining JSX and text e.g. <Foo>{bar}
then the helpful error message is not shown.
These kinds of errors are easy to solve in simple JSX but can be really challenging to spot when the offending code is in the middle of complex TS code, because it's not obvious that the problem is JSX-related instead of "something in my TS code is broken".
The root cause is a misunderstanding between the developer (who's assuming code to be in a JSX expression context) and the compiler (which knows that the code is NOT in a JSX expression context.
From the developer's point of view, the fix for these problems is simple: wrap the offending TS code in a Fragment or another element. This gave me an idea for how TS could perhaps show smarter error messages. Could the TS compiler recognize cases where the following two conditions are true?
- the developer may have expected code to be evaluated as a JSX expression, e.g. the code is inside a JSX element, or the code has a JSX element as a sibling.
- the compiler errors go away if the code is treated as a JSX expression instead of as regular TS code
If so, then the TS compiler could show an error message (e.g. "JSX expressions must be wrapped in a fragment or JSX element") at the start or end of the offending code. Even if there are false positives-- e.g. legitimate code bugs inside a {code} block inside some JSX-- it's probably better to give devs multiple possible reasons for a compiler error than to omit a likely reason.
For example, here's two patterns I've run across recently:
- Pattern A: An inline function is defined inside a JSX element (or fragment) AND and there's a compiler error in that code AND the errors go away if the function's body were wrapped in a fragment. Examples:
Test #1: GOOD
<>
{data.map((str: string, i: number) => (
{str} // no error
))}
</>
Test #2: GOOD
<>{data.map((str: string, i: number) => (
<>{i}: {str}</> // no error
))}
</>
Test #3: BAD
<>
{data.map((str: string, i: number) => (
{i}: {str} // errors: [ts] ')' expected. [1005]; [ts] Cannot find name 'str'. [2304]
))}
</>
- Pattern B: A JSX element (wrapped in
<
and>
) is immediately preceded and/or immediately followed by non-JSX code that generates a compiler error AND the code would not throw a compiler error if it were wrapped in a Fragment.
Test #4: GOOD
<>
{data.map((str: string, i: number) => (
{i}+": "+{str}+"\n" // no error
))}
</>
Test #5: GOOD
<>
{data.map((str: string, i: number) => (
<br/><>{i}+": "+{str}+"\n"</> // error on <br/>: [ts] Left side of comma operator is unused and has no side effects. [2695]
)) // Good error here: [ts] JSX expressions must have one parent element. [2657]
}
</>
Test #6: BAD
<>
{data.map((str: string, i: number) => (
<br/>{i}+": "+{str}+"\n" // errors: [ts] Cannot find name 'i'. [2304]; [ts] Cannot find name 'str'. [2304]
)) // error (on second parenthesis): [ts] '}' expected. [1005]
}
</>
Examples
Here's the full example from above.
const repro = () => {
const simpleJSX = (
<div>hi</div> // confusing error here: [ts] Left side of comma operator is unused and has no side effects. [2695]
<div>bye</div>
); // Good error here: [ts] JSX expressions must have one parent element. [2657]
const data = ['a', 'b', 'c'];
return (
<>
Test #1: GOOD
<>
{data.map((str: string, i: number) => (
{str} // no error
))}
</>
Test #2: GOOD
<>{data.map((str: string, i: number) => (
<>{i}: {str}</> // no error
))}
</>
Test #3: BAD
<>
{data.map((str: string, i: number) => (
{i}: {str} // errors: [ts] ')' expected. [1005]; [ts] Cannot find name 'str'. [2304]
))}
</>
Test #4: GOOD
<>
{data.map((str: string, i: number) => (
{i}+": "+{str}+"\n" // no error
))}
</>
Test #5: GOOD
<>
{data.map((str: string, i: number) => (
<br/><>{i}+": "+{str}+"\n"</> // error on <br/>: [ts] Left side of comma operator is unused and has no side effects. [2695]
)) // Good error here: [ts] JSX expressions must have one parent element. [2657]
}
</>
Test #6: BAD
<>
{data.map((str: string, i: number) => (
<br/>{i}+": "+{str}+"\n" // errors: [ts] Cannot find name 'i'. [2304]; [ts] Cannot find name 'str'. [2304]
)) // error (on second parenthesis): [ts] '}' expected. [1005]
}
</>
</>
)};
Checklist
My suggestion meets these guidelines:
-
This wouldn't be a breaking change in existing TypeScript/JavaScript code
(because it only applies to code that already has a compiler error) -
This wouldn't change the runtime behavior of existing JavaScript code
-
This could be implemented without emitting different JS based on the types of the expressions
-
This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
-
This feature would agree with the rest of TypeScript's Design Goals.