skip to content

In a .tsx file, `const identity = <T>(value: T) => value;` fails to compile although the identical line is fine in a .ts file. Why, and what are two ways to write it so it parses?

level: juniorimportance: should knowfreq 45%

answer

  1. the file extension changes the grammar
  2. angle brackets are claimed by something else
  3. the parser wants proof it is not a tag
  4. a comma, a constraint, or the function keyword
  5. same reason angle-bracket assertions are banned there

basics

~20 s

In .tsx files the parser reads the leading angle bracket as the start of a JSX element, not a type parameter list. Disambiguate with a trailing comma, <T,>, add a constraint such as <T extends unknown>, or use function syntax.

solid answer

~40 s

`.tsx` files enable JSX, which claims the angle brackets. When the parser meets `<T>` at the start of an expression it commits to a JSX element and then complains that the tag is never closed — nothing about generics is actually wrong. The standard fixes are all about giving the parser something that cannot be a tag: write the trailing comma, `const identity = <T,>(value: T) => value;`, add a constraint, `<T extends unknown>(value: T) => value`, or declare it with `function identity<T>(value: T) { return value; }`, whose type parameter list is never ambiguous. The same conflict is why the old angle-bracket assertion `<string>value` is unavailable in `.tsx` and you must write `value as string` there.

code

typescript · 15 lines
typescript
// In a .tsx file the parser reads <T> as a JSX tag:
// const bad = <T>(value: T) => value;   // error: unclosed tag

// Fix 1: a trailing comma cannot appear in a tag name
const identity = <T,>(value: T) => value;

// Fix 2: a constraint also rules out the JSX branch
const first = <T extends unknown>(items: T[]): T | undefined => items[0];

// Fix 3: function syntax is never ambiguous
function last<T>(items: T[]): T | undefined {
  return items[items.length - 1];
}

console.log(identity(1), first(["a"]), last([true]));

go deeper

for a junior

Recognise the JSX parse error for what it is and remember one working spelling — the trailing comma in <T,> — so the error never costs you more than a few seconds.

for a middle

Explain that this is a grammar conflict introduced by enabling JSX, list the three disambiguating forms, and connect it to why as is the only assertion syntax available in .tsx.

for a senior

Have a position for the codebase: prefer function declarations for generic components so the issue cannot recur, and know that formatter or parser misconfiguration is what usually reintroduces it.

for a principal

Frame it as a file-convention decision — which extension a file gets, whether JSX is enabled project-wide, and what the lint rules say about assertion syntax — so individual authors never have to rediscover the workaround.

## Two grammars competing for one character TypeScript reuses `<` and `>` for two unrelated things: type parameter and type argument lists, and — when JSX is enabled — element tags. In a `.ts` file only the first meaning exists, so `<T>(value: T) => value` is unambiguously a generic arrow function. In a `.tsx` file the parser sees `<` in expression position and takes the JSX branch: `T` becomes a tag name, `(value: T) => value` becomes children, and the file ends without a closing `</T>`. The error you get is about JSX, which is why it reads as nonsense to someone who thinks they wrote a generics problem. This is a pure *parsing* rule. Nothing about generic arrow functions is restricted in `.tsx`; you only have to write them in a form the parser cannot mistake for markup. ## The three forms that work ```typescript // 1) trailing comma: a tag name cannot be followed by a comma const identity = <T,>(value: T) => value; // 2) a constraint: `extends` cannot appear in a tag const first = <T extends unknown>(items: T[]): T | undefined => items[0]; // 3) function syntax: the name comes before the angle brackets function last<T>(items: T[]): T | undefined { return items[items.length - 1]; } ``` The trailing comma is the most common in modern codebases because it is minimal and says nothing false about the type. The `extends unknown` form is the older idiom and is equivalent in meaning — `unknown` is the top type, so it constrains nothing — but readers sometimes mistake it for a deliberate bound. A real constraint (`<T extends object>`) also disambiguates, but pick it because the code needs the bound, not to satisfy the parser. Function declarations and function expressions sidestep the issue entirely, which is one reason many teams write generic components as `function` declarations by default. ## The same rule, elsewhere The angle brackets are contested in one more place. TypeScript has two assertion syntaxes: ```typescript const a = value as string; // works everywhere // const b = <string>value; // parse error in .tsx: reads as a JSX tag ``` So `.tsx` files must use `as`. This is worth knowing as the same phenomenon rather than a separate rule: JSX has taken the angle brackets, and every construct that wanted them needs another spelling. Since `as` works in `.ts` too, most style guides simply ban the angle-bracket form outright. Type *arguments* at a call site are not affected — `identity<string>("x")` parses fine in `.tsx`, because the `<` follows an expression rather than opening one. The ambiguity only exists where an expression begins. ## Where it bites in practice The rule shows up the moment someone writes a generic component or hook as an arrow constant, which is the dominant style in many frontend codebases: ```typescript type Props<T> = { items: readonly T[]; renderItem: (item: T) => string }; const List = <T,>({ items, renderItem }: Props<T>): string => items.map(renderItem).join(""); ``` Without the comma this is a wall of JSX errors, and formatters occasionally strip a lone trailing comma if they are configured for a non-TSX parser — a recurring source of "it compiled yesterday" confusion. When that happens, converting to `function` syntax is the durable fix rather than fighting the formatter. ## What interviewers are checking This is a recall question with a short right answer, and the failure mode is a candidate who invents a semantic explanation — "arrow functions can't be generic", "you need a constraint under strict mode", "JSX erases type parameters". None of those are true. The expected answer is one sentence about the parser plus at least one working spelling, and a strong candidate adds that this is the same reason `as` is mandatory for assertions in `.tsx`.

  • Does `<T extends unknown>` constrain anything, and why do people write it?
    It constrains nothing — `unknown` is the top type, so every type satisfies it. It exists purely as a parse disambiguator: `extends` cannot appear inside a JSX tag, so the parser commits to a type parameter list. It predates the trailing-comma idiom and survives in older code; prefer `<T,>` when you have no real bound, and a genuine constraint when you do.
  • Is type-argument syntax at a call site affected in .tsx as well?
    No. `identity<string>("x")` parses fine, because the `<` follows an expression rather than starting one, so the JSX branch is never taken. The ambiguity only exists where an expression begins — which is exactly the arrow-function case and the old angle-bracket assertion. That is why `as` is required in `.tsx` while explicit type arguments need no workaround.

saying these in an interview costs you the question

  • Claims arrow functions cannot be generic in TypeScript
  • Says a constraint is required by strict mode
  • Thinks the file extension changes type checking, not parsing
  • Believes the trailing comma changes the type parameter's meaning
  • Uses angle-bracket assertions in .tsx and blames the compiler

context