TIL about TypeScript and TSX
Today we noticed an issue in Val Town’s TypeScript support.
// The 'bug' was that source code like:
const x = <X>(arg: X) => {};
// Was getting formatted to this:
const x = <X,>(arg: X) => {};This is an arrow function with a generic argument called X.
Interesting! Why would dprint want to insert a trailing comma in a generic? I discovered this fascinating issue.
Let's say that the input was this instead:
export const SelectRow = <T>(row: Row<T>) => false;Some people have probably guessed the issue already: this is colliding with TSX syntax. If that line of code were like
export const SelectRow = <T>something</T>;Then you'd expect this to be an element with a tag name T, rather than T as a generic. The syntaxes collide! How is TypeScript to know as it is parsing the code whether it's looking at JSX or a generic argument?
The comma lets TypeScript know that it’s a generic argument! If you check out some behavior:
- prettier adds the comma in a generic
- biome’s parser does not allow the generic without a comma
- dprint adds the comma
- typescript doesn’t parse it
So, if you're using generics with arrow functions in TypeScript and your file is a TSX file, that comma is pretty important, and intentional!
For us, the issue was that we were relying on CodeMirror’s syntax tree to check for the presence of JSX syntax, and it was incorrectly parsing arrow functions with generic arguments and the trailing comma as JSX. One of CodeMirror's dependencies, @lezer/javascript, was out of date: they had fixed this bug late last year. Fixing that squashed the false warning in the UI and let the <T,> pattern work as intended.
Looking at this and the history of the lezer package is a reminder of how TypeScript is quite a complicated language and it adds features at a sort of alarming rate. It's fantastic to use as a developer, but building 'meta' tools around it is pretty challenging, because there are a lot of tricky details like this one.
What makes this even a bit trickier is that if you are writing a .ts file for TypeScript, with no JSX support, then generic without the extra comma is valid syntax:
const x = <X>(y: X) => true;For what it's worth, I don't use arrow functions that often - I like function declarations more because of hoisting, support for generators, and their kludgy-but-explicit syntax. For example, arrow functions also have a little syntax collision even if you don't consider TypeScript: you can generally omit the {} brackets around the arrow function return value, like
const x = () => 1;But what if it's an object that you're trying to return? If you write
const x = () => { y: 1 };Then you may be surprised to learn that you aren't returning an object like { y: 1 }, but you've defined a labeled statement and the function x will, in fact, return undefined. You can return an object from an arrow function, but you have to parenthesize it:
const x = () => ({ y: 1 });So I kind of like function declarations because I just don't have to think about these things that often.
Thanks David Siegel for reporting this issue!