diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c48c4c8e06bcd..c3a3f2b6416c3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -12187,7 +12187,11 @@ namespace ts { function checkJsxExpression(node: JsxExpression) { if (node.expression) { - return checkExpression(node.expression); + const type = checkExpression(node.expression); + if (node.dotDotDotToken && type !== anyType && !isArrayType(type)) { + error(node, Diagnostics.JSX_spread_child_must_be_an_array_type, node.toString(), typeToString(type)); + } + return type; } else { return unknownType; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f275760e3cbab..7e8dc1ce20924 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1811,6 +1811,10 @@ "category": "Error", "code": 2608 }, + "JSX spread child must be an array type.": { + "category": "Error", + "code": 2609 + }, "Cannot emit namespaced JSX elements in React": { "category": "Error", "code": 2650 diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 2f19c9b3d3206..0a03ec6863ffd 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1854,6 +1854,9 @@ namespace ts { function emitJsxExpression(node: JsxExpression) { if (node.expression) { write("{"); + if (node.dotDotDotToken) { + write("..."); + } emitExpression(node.expression); write("}"); } diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index ebcea221f9e8c..c2535cacbe554 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -1317,15 +1317,16 @@ namespace ts { return node; } - export function createJsxExpression(expression: Expression, location?: TextRange) { + export function createJsxExpression(expression: Expression, dotDotDotToken: Token, location?: TextRange) { const node = createNode(SyntaxKind.JsxExpression, location); + node.dotDotDotToken = dotDotDotToken; node.expression = expression; return node; } export function updateJsxExpression(node: JsxExpression, expression: Expression) { if (node.expression !== expression) { - return updateNode(createJsxExpression(expression, node), node); + return updateNode(createJsxExpression(expression, node.dotDotDotToken, node), node); } return node; } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 2f0d6a4d9754e..8019b864ad50e 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -373,7 +373,8 @@ namespace ts { case SyntaxKind.JsxSpreadAttribute: return visitNode(cbNode, (node).expression); case SyntaxKind.JsxExpression: - return visitNode(cbNode, (node).expression); + return visitNode(cbNode, (node as JsxExpression).dotDotDotToken) || + visitNode(cbNode, (node as JsxExpression).expression); case SyntaxKind.JsxClosingElement: return visitNode(cbNode, (node).tagName); @@ -3914,6 +3915,7 @@ namespace ts { parseExpected(SyntaxKind.OpenBraceToken); if (token() !== SyntaxKind.CloseBraceToken) { + node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); node.expression = parseAssignmentExpressionOrHigher(); } if (inExpressionContext) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1ee59149b2c5c..ee432b13240e0 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1492,6 +1492,7 @@ namespace ts { export interface JsxExpression extends Expression { kind: SyntaxKind.JsxExpression; + dotDotDotToken?: Token; expression?: Expression; } diff --git a/tests/baselines/reference/tsxSpreadChildren.js b/tests/baselines/reference/tsxSpreadChildren.js new file mode 100644 index 0000000000000..bcc62de056670 --- /dev/null +++ b/tests/baselines/reference/tsxSpreadChildren.js @@ -0,0 +1,41 @@ +//// [tsxSpreadChildren.tsx] + +declare module JSX { + interface Element { } + interface IntrinsicElements { + [s: string]: any; + } +} +declare var React: any; + +interface TodoProp { + id: number; + todo: string; +} +interface TodoListProps { + todos: TodoProp[]; +} +function Todo(prop: { key: number, todo: string }) { + return
{prop.key.toString() + prop.todo}
; +} +function TodoList({ todos }: TodoListProps) { + return
+ {...todos.map(todo => )} +
; +} +let x: TodoListProps; + + + +//// [tsxSpreadChildren.jsx] +function Todo(prop) { + return
{prop.key.toString() + prop.todo}
; +} +function TodoList(_a) { + var todos = _a.todos; + return
+ {...todos.map(function (todo) { return ; })} +
; +} +var x; +; diff --git a/tests/baselines/reference/tsxSpreadChildren.symbols b/tests/baselines/reference/tsxSpreadChildren.symbols new file mode 100644 index 0000000000000..6e726e932179b --- /dev/null +++ b/tests/baselines/reference/tsxSpreadChildren.symbols @@ -0,0 +1,86 @@ +=== tests/cases/conformance/jsx/tsxSpreadChildren.tsx === + +declare module JSX { +>JSX : Symbol(JSX, Decl(tsxSpreadChildren.tsx, 0, 0)) + + interface Element { } +>Element : Symbol(Element, Decl(tsxSpreadChildren.tsx, 1, 20)) + + interface IntrinsicElements { +>IntrinsicElements : Symbol(IntrinsicElements, Decl(tsxSpreadChildren.tsx, 2, 22)) + + [s: string]: any; +>s : Symbol(s, Decl(tsxSpreadChildren.tsx, 4, 3)) + } +} +declare var React: any; +>React : Symbol(React, Decl(tsxSpreadChildren.tsx, 7, 11)) + +interface TodoProp { +>TodoProp : Symbol(TodoProp, Decl(tsxSpreadChildren.tsx, 7, 23)) + + id: number; +>id : Symbol(TodoProp.id, Decl(tsxSpreadChildren.tsx, 9, 20)) + + todo: string; +>todo : Symbol(TodoProp.todo, Decl(tsxSpreadChildren.tsx, 10, 15)) +} +interface TodoListProps { +>TodoListProps : Symbol(TodoListProps, Decl(tsxSpreadChildren.tsx, 12, 1)) + + todos: TodoProp[]; +>todos : Symbol(TodoListProps.todos, Decl(tsxSpreadChildren.tsx, 13, 25)) +>TodoProp : Symbol(TodoProp, Decl(tsxSpreadChildren.tsx, 7, 23)) +} +function Todo(prop: { key: number, todo: string }) { +>Todo : Symbol(Todo, Decl(tsxSpreadChildren.tsx, 15, 1)) +>prop : Symbol(prop, Decl(tsxSpreadChildren.tsx, 16, 14)) +>key : Symbol(key, Decl(tsxSpreadChildren.tsx, 16, 21)) +>todo : Symbol(todo, Decl(tsxSpreadChildren.tsx, 16, 34)) + + return
{prop.key.toString() + prop.todo}
; +>div : Symbol(JSX.IntrinsicElements, Decl(tsxSpreadChildren.tsx, 2, 22)) +>prop.key.toString : Symbol(Number.toString, Decl(lib.d.ts, --, --)) +>prop.key : Symbol(key, Decl(tsxSpreadChildren.tsx, 16, 21)) +>prop : Symbol(prop, Decl(tsxSpreadChildren.tsx, 16, 14)) +>key : Symbol(key, Decl(tsxSpreadChildren.tsx, 16, 21)) +>toString : Symbol(Number.toString, Decl(lib.d.ts, --, --)) +>prop.todo : Symbol(todo, Decl(tsxSpreadChildren.tsx, 16, 34)) +>prop : Symbol(prop, Decl(tsxSpreadChildren.tsx, 16, 14)) +>todo : Symbol(todo, Decl(tsxSpreadChildren.tsx, 16, 34)) +>div : Symbol(JSX.IntrinsicElements, Decl(tsxSpreadChildren.tsx, 2, 22)) +} +function TodoList({ todos }: TodoListProps) { +>TodoList : Symbol(TodoList, Decl(tsxSpreadChildren.tsx, 18, 1)) +>todos : Symbol(todos, Decl(tsxSpreadChildren.tsx, 19, 19)) +>TodoListProps : Symbol(TodoListProps, Decl(tsxSpreadChildren.tsx, 12, 1)) + + return
+>div : Symbol(JSX.IntrinsicElements, Decl(tsxSpreadChildren.tsx, 2, 22)) + + {...todos.map(todo => )} +>todos.map : Symbol(Array.map, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>todos : Symbol(todos, Decl(tsxSpreadChildren.tsx, 19, 19)) +>map : Symbol(Array.map, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>todo : Symbol(todo, Decl(tsxSpreadChildren.tsx, 21, 22)) +>Todo : Symbol(Todo, Decl(tsxSpreadChildren.tsx, 15, 1)) +>key : Symbol(key, Decl(tsxSpreadChildren.tsx, 16, 21)) +>todo.id : Symbol(TodoProp.id, Decl(tsxSpreadChildren.tsx, 9, 20)) +>todo : Symbol(todo, Decl(tsxSpreadChildren.tsx, 21, 22)) +>id : Symbol(TodoProp.id, Decl(tsxSpreadChildren.tsx, 9, 20)) +>todo : Symbol(todo, Decl(tsxSpreadChildren.tsx, 16, 34)) +>todo.todo : Symbol(TodoProp.todo, Decl(tsxSpreadChildren.tsx, 10, 15)) +>todo : Symbol(todo, Decl(tsxSpreadChildren.tsx, 21, 22)) +>todo : Symbol(TodoProp.todo, Decl(tsxSpreadChildren.tsx, 10, 15)) + +
; +>div : Symbol(JSX.IntrinsicElements, Decl(tsxSpreadChildren.tsx, 2, 22)) +} +let x: TodoListProps; +>x : Symbol(x, Decl(tsxSpreadChildren.tsx, 24, 3)) +>TodoListProps : Symbol(TodoListProps, Decl(tsxSpreadChildren.tsx, 12, 1)) + + +>TodoList : Symbol(TodoList, Decl(tsxSpreadChildren.tsx, 18, 1)) +>x : Symbol(x, Decl(tsxSpreadChildren.tsx, 24, 3)) + diff --git a/tests/baselines/reference/tsxSpreadChildren.types b/tests/baselines/reference/tsxSpreadChildren.types new file mode 100644 index 0000000000000..fbf76d0679d18 --- /dev/null +++ b/tests/baselines/reference/tsxSpreadChildren.types @@ -0,0 +1,94 @@ +=== tests/cases/conformance/jsx/tsxSpreadChildren.tsx === + +declare module JSX { +>JSX : any + + interface Element { } +>Element : Element + + interface IntrinsicElements { +>IntrinsicElements : IntrinsicElements + + [s: string]: any; +>s : string + } +} +declare var React: any; +>React : any + +interface TodoProp { +>TodoProp : TodoProp + + id: number; +>id : number + + todo: string; +>todo : string +} +interface TodoListProps { +>TodoListProps : TodoListProps + + todos: TodoProp[]; +>todos : TodoProp[] +>TodoProp : TodoProp +} +function Todo(prop: { key: number, todo: string }) { +>Todo : (prop: { key: number; todo: string; }) => JSX.Element +>prop : { key: number; todo: string; } +>key : number +>todo : string + + return
{prop.key.toString() + prop.todo}
; +>
{prop.key.toString() + prop.todo}
: JSX.Element +>div : any +>prop.key.toString() + prop.todo : string +>prop.key.toString() : string +>prop.key.toString : (radix?: number) => string +>prop.key : number +>prop : { key: number; todo: string; } +>key : number +>toString : (radix?: number) => string +>prop.todo : string +>prop : { key: number; todo: string; } +>todo : string +>div : any +} +function TodoList({ todos }: TodoListProps) { +>TodoList : ({todos}: TodoListProps) => JSX.Element +>todos : TodoProp[] +>TodoListProps : TodoListProps + + return
+>
{...todos.map(todo => )}
: JSX.Element +>div : any + + {...todos.map(todo => )} +>todos.map(todo => ) : JSX.Element[] +>todos.map : { (this: [TodoProp, TodoProp, TodoProp, TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U, U, U, U]; (this: [TodoProp, TodoProp, TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U, U, U]; (this: [TodoProp, TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U, U]; (this: [TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U]; (callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): U[]; } +>todos : TodoProp[] +>map : { (this: [TodoProp, TodoProp, TodoProp, TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U, U, U, U]; (this: [TodoProp, TodoProp, TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U, U, U]; (this: [TodoProp, TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U, U]; (this: [TodoProp, TodoProp], callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): [U, U]; (callbackfn: (value: TodoProp, index: number, array: TodoProp[]) => U, thisArg?: any): U[]; } +>todo => : (todo: TodoProp) => JSX.Element +>todo : TodoProp +> : JSX.Element +>Todo : (prop: { key: number; todo: string; }) => JSX.Element +>key : any +>todo.id : number +>todo : TodoProp +>id : number +>todo : any +>todo.todo : string +>todo : TodoProp +>todo : string + +
; +>div : any +} +let x: TodoListProps; +>x : TodoListProps +>TodoListProps : TodoListProps + + +> : JSX.Element +>TodoList : ({todos}: TodoListProps) => JSX.Element +>x : TodoListProps + diff --git a/tests/baselines/reference/tsxSpreadChildrenInvalidType.errors.txt b/tests/baselines/reference/tsxSpreadChildrenInvalidType.errors.txt new file mode 100644 index 0000000000000..d1524c6bf3a97 --- /dev/null +++ b/tests/baselines/reference/tsxSpreadChildrenInvalidType.errors.txt @@ -0,0 +1,38 @@ +tests/cases/conformance/jsx/tsxSpreadChildrenInvalidType.tsx(21,9): error TS2609: JSX spread child must be an array type. + + +==== tests/cases/conformance/jsx/tsxSpreadChildrenInvalidType.tsx (1 errors) ==== + declare module JSX { + interface Element { } + interface IntrinsicElements { + [s: string]: any; + } + } + declare var React: any; + + interface TodoProp { + id: number; + todo: string; + } + interface TodoListProps { + todos: TodoProp[]; + } + function Todo(prop: { key: number, todo: string }) { + return
{prop.key.toString() + prop.todo}
; + } + function TodoList({ todos }: TodoListProps) { + return
+ {...} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2609: JSX spread child must be an array type. +
; + } + function TodoListNoError({ todos }: TodoListProps) { + // any is not checked + return
+ {...( as any)} +
; + } + let x: TodoListProps; + + \ No newline at end of file diff --git a/tests/baselines/reference/tsxSpreadChildrenInvalidType.js b/tests/baselines/reference/tsxSpreadChildrenInvalidType.js new file mode 100644 index 0000000000000..e9873bfb233a6 --- /dev/null +++ b/tests/baselines/reference/tsxSpreadChildrenInvalidType.js @@ -0,0 +1,57 @@ +//// [tsxSpreadChildrenInvalidType.tsx] +declare module JSX { + interface Element { } + interface IntrinsicElements { + [s: string]: any; + } +} +declare var React: any; + +interface TodoProp { + id: number; + todo: string; +} +interface TodoListProps { + todos: TodoProp[]; +} +function Todo(prop: { key: number, todo: string }) { + return
{prop.key.toString() + prop.todo}
; +} +function TodoList({ todos }: TodoListProps) { + return
+ {...} +
; +} +function TodoListNoError({ todos }: TodoListProps) { + // any is not checked + return
+ {...( as any)} +
; +} +let x: TodoListProps; + + + +//// [tsxSpreadChildrenInvalidType.js] +var __assign = (this && this.__assign) || Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; +}; +function Todo(prop) { + return React.createElement("div", null, prop.key.toString() + prop.todo); +} +function TodoList(_a) { + var todos = _a.todos; + return React.createElement("div", null, React.createElement(Todo, { key: todos[0].id, todo: todos[0].todo })); +} +function TodoListNoError(_a) { + var todos = _a.todos; + // any is not checked + return React.createElement("div", null, React.createElement(Todo, { key: todos[0].id, todo: todos[0].todo })); +} +var x; +React.createElement(TodoList, __assign({}, x)); diff --git a/tests/cases/conformance/jsx/tsxSpreadChildren.tsx b/tests/cases/conformance/jsx/tsxSpreadChildren.tsx new file mode 100644 index 0000000000000..3be0b3b99e459 --- /dev/null +++ b/tests/cases/conformance/jsx/tsxSpreadChildren.tsx @@ -0,0 +1,27 @@ +//@jsx: preserve + +declare module JSX { + interface Element { } + interface IntrinsicElements { + [s: string]: any; + } +} +declare var React: any; + +interface TodoProp { + id: number; + todo: string; +} +interface TodoListProps { + todos: TodoProp[]; +} +function Todo(prop: { key: number, todo: string }) { + return
{prop.key.toString() + prop.todo}
; +} +function TodoList({ todos }: TodoListProps) { + return
+ {...todos.map(todo => )} +
; +} +let x: TodoListProps; + diff --git a/tests/cases/conformance/jsx/tsxSpreadChildrenInvalidType.tsx b/tests/cases/conformance/jsx/tsxSpreadChildrenInvalidType.tsx new file mode 100644 index 0000000000000..41181618ce0a3 --- /dev/null +++ b/tests/cases/conformance/jsx/tsxSpreadChildrenInvalidType.tsx @@ -0,0 +1,32 @@ +// @jsx: react +declare module JSX { + interface Element { } + interface IntrinsicElements { + [s: string]: any; + } +} +declare var React: any; + +interface TodoProp { + id: number; + todo: string; +} +interface TodoListProps { + todos: TodoProp[]; +} +function Todo(prop: { key: number, todo: string }) { + return
{prop.key.toString() + prop.todo}
; +} +function TodoList({ todos }: TodoListProps) { + return
+ {...} +
; +} +function TodoListNoError({ todos }: TodoListProps) { + // any is not checked + return
+ {...( as any)} +
; +} +let x: TodoListProps; +