From 57a3e46aaf7f6803914bc8671fef5448333503d7 Mon Sep 17 00:00:00 2001 From: Tycho Grouwstra Date: Thu, 21 Sep 2017 22:11:49 +0800 Subject: [PATCH] allow spread in tuple types --- src/compiler/binder.ts | 1 + src/compiler/checker.ts | 93 ++++++++++++++++++- src/compiler/diagnosticMessages.json | 4 + src/compiler/emitter.ts | 9 ++ src/compiler/factory.ts | 17 +++- src/compiler/parser.ts | 20 +++- src/compiler/types.ts | 15 ++- src/compiler/utilities.ts | 4 + src/compiler/visitor.ts | 8 ++ tests/baselines/reference/tupleTypeSpread.js | 7 ++ .../reference/tupleTypeSpread.symbols | 15 +++ .../baselines/reference/tupleTypeSpread.types | 15 +++ .../tupleTypeSpreadErrors.errors.txt | 8 ++ .../reference/tupleTypeSpreadErrors.js | 5 + tests/cases/compiler/tupleTypeSpread.ts | 4 + tests/cases/compiler/tupleTypeSpreadErrors.ts | 1 + 16 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 tests/baselines/reference/tupleTypeSpread.js create mode 100644 tests/baselines/reference/tupleTypeSpread.symbols create mode 100644 tests/baselines/reference/tupleTypeSpread.types create mode 100644 tests/baselines/reference/tupleTypeSpreadErrors.errors.txt create mode 100644 tests/baselines/reference/tupleTypeSpreadErrors.js create mode 100644 tests/cases/compiler/tupleTypeSpread.ts create mode 100644 tests/cases/compiler/tupleTypeSpreadErrors.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 97e6077715924..07dae5a0c1f57 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -3351,6 +3351,7 @@ namespace ts { case SyntaxKind.TypeLiteral: case SyntaxKind.ArrayType: case SyntaxKind.TupleType: + case SyntaxKind.TypeSpread: case SyntaxKind.UnionType: case SyntaxKind.IntersectionType: case SyntaxKind.ParenthesizedType: diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e00465caa146a..9a2dac068016b 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2555,6 +2555,10 @@ namespace ts { const indexTypeNode = typeToTypeNodeHelper((type).indexType, context); return createIndexedAccessTypeNode(objectTypeNode, indexTypeNode); } + if (type.flags & TypeFlags.SpreadTuple) { + const typeNodes = map((type).elements, (tp: Type) => typeToTypeNodeHelper(tp, context)); + return createTupleTypeNode(typeNodes, (type).idxIsSpread); + } Debug.fail("Should be unreachable."); @@ -3324,6 +3328,12 @@ namespace ts { writeType((type).indexType, TypeFormatFlags.None); writePunctuation(writer, SyntaxKind.CloseBracketToken); } + else if (type.flags & TypeFlags.SpreadTuple) { + writePunctuation(writer, SyntaxKind.OpenBracketToken); + writePunctuation(writer, SyntaxKind.DotDotDotToken); + writeTypeList((type).elements, SyntaxKind.CommaToken, (type).idxIsSpread); + writePunctuation(writer, SyntaxKind.CloseBracketToken); + } else { // Should never get here // { ... } @@ -3336,7 +3346,7 @@ namespace ts { } - function writeTypeList(types: Type[], delimiter: SyntaxKind) { + function writeTypeList(types: Type[], delimiter: SyntaxKind, idxIsSpread: boolean[] = []) { for (let i = 0; i < types.length; i++) { if (i > 0) { if (delimiter !== SyntaxKind.CommaToken) { @@ -3345,6 +3355,9 @@ namespace ts { writePunctuation(writer, delimiter); writeSpace(writer); } + if (idxIsSpread[i]) { + writePunctuation(writer, SyntaxKind.DotDotDotToken); + } writeType(types[i], delimiter === SyntaxKind.CommaToken ? TypeFormatFlags.None : TypeFormatFlags.InElementType); } } @@ -7262,11 +7275,67 @@ namespace ts { function getTypeFromTupleTypeNode(node: TupleTypeNode): Type { const links = getNodeLinks(node); if (!links.resolvedType) { - links.resolvedType = createTupleType(map(node.elementTypes, getTypeFromTypeNode)); + links.resolvedType = getTypeForTupleNode(node); } return links.resolvedType; } + function getSpreadTupleTypes(elements: Type[], idxIsSpread: boolean[]): Type { + return createTupleType(flatMap(elements, (tp: Type, i: number) => idxIsSpread[i] ? getTypeSpreadTypes(tp) : tp)); + } + + function getTypeSpreadTypes(tuple: Type): Type[] { + if (isTupleLikeType(tuple)) { + return getTupleTypeElementTypes(tuple); + } + else { + // console.error("not a tuple, don't resolve?"); + return []; + } + } + + function isGenericTupleType(type: Type): boolean { + return type.flags & TypeFlags.TypeVariable ? true : + type.flags & TypeFlags.UnionOrIntersection ? forEach((type).types, isGenericTupleType) : + false; + } + + function getTypeForTupleNode(node: TupleTypeNode): Type { + if (some(node.elementTypes, (n: TypeNode) => n.kind === SyntaxKind.TypeSpread && + isGenericTupleType(getTypeFromTypeNode((n as TypeSpreadTypeNode).type)))) { + const elements = map(node.elementTypes, (n: TypeNode) => getTypeFromTypeNode(n.kind === SyntaxKind.TypeSpread ? (n as TypeSpreadTypeNode).type : n)); + const idxIsSpread = map(node.elementTypes, (n: TypeNode) => n.kind === SyntaxKind.TypeSpread); + return createTupleSpreadType(elements, idxIsSpread); + } + else { + return createTupleType(flatMap(node.elementTypes, getTypeFromTupleElement)); + } + } + + function getTupleTypeElementTypes(type: Type): Type[] { + Debug.assert(isTupleLikeType(type)); + const types = []; + let idx = 0; + let symbol: Symbol; + while (symbol = getPropertyOfObjectType(type, idx++ + "" as __String)) { + types.push(getTypeOfSymbol(symbol)); + } + return types; + } + + function getTypeFromTupleElement(node: TypeNode | TypeSpreadTypeNode): Type | Type[] { + if (node.kind === SyntaxKind.TypeSpread) { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getTypeFromTypeNode((node as TypeSpreadTypeNode).type); + } + return getTypeSpreadTypes(links.resolvedType); + } + else { + return getTypeFromTypeNode(node as TypeNode); + } + } + interface TypeSet extends Array { containsAny?: boolean; containsUndefined?: boolean; @@ -7630,6 +7699,13 @@ namespace ts { return type; } + function createTupleSpreadType(elements: Type[], idxIsSpread: boolean[]) { + const type = createType(TypeFlags.SpreadTuple); + type.elements = elements; + type.idxIsSpread = idxIsSpread; + return type; + } + function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode, cacheSymbol: boolean) { const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; const propName = indexType.flags & TypeFlags.StringOrNumberLiteral ? @@ -8366,6 +8442,9 @@ namespace ts { return getIndexedAccessType(instantiateType((type).objectType, mapper), instantiateType((type).indexType, mapper)); } } + if (type.flags & TypeFlags.SpreadTuple) { + return getSpreadTupleTypes(instantiateTypes((type).elements, mapper), (type).idxIsSpread); + } return type; } @@ -18930,6 +19009,14 @@ namespace ts { forEach(node.elementTypes, checkSourceElement); } + function checkTypeSpreadTypeNode(node: TypeSpreadTypeNode) { + checkSourceElement(node.type); + const type = getApparentType(getTypeFromTypeNode(node.type)); + if (!isArrayLikeType(type)) { // isTupleLikeType + grammarErrorOnNode(node, Diagnostics.Tuple_type_spreads_may_only_be_created_from_tuple_types); + } + } + function checkUnionOrIntersectionType(node: UnionOrIntersectionTypeNode) { forEach(node.types, checkSourceElement); } @@ -22494,6 +22581,8 @@ namespace ts { return checkArrayType(node); case SyntaxKind.TupleType: return checkTupleType(node); + case SyntaxKind.TypeSpread: + return checkTypeSpreadTypeNode(node); case SyntaxKind.UnionType: case SyntaxKind.IntersectionType: return checkUnionOrIntersectionType(node); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 662e87d3159af..f22ea78f5ccfe 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2216,6 +2216,10 @@ "category": "Error", "code": 2714 }, + "Tuple type spreads may only be created from tuple types.": { + "category": "Error", + "code": 2715 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 502329bbd8422..0c737f53cc29c 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -712,6 +712,8 @@ namespace ts { return emitShorthandPropertyAssignment(node); case SyntaxKind.SpreadAssignment: return emitSpreadAssignment(node as SpreadAssignment); + case SyntaxKind.TypeSpread: + return emitTypeSpread(node as TypeSpreadTypeNode); // Enum case SyntaxKind.EnumMember: @@ -2183,6 +2185,13 @@ namespace ts { } } + function emitTypeSpread(node: TypeSpreadTypeNode) { + if (node.type) { + write("..."); + emit(node.type); + } + } + // // Enum // diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 1b412198e5efa..8a52d84ab7528 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -674,9 +674,10 @@ namespace ts { : node; } - export function createTupleTypeNode(elementTypes: ReadonlyArray) { + export function createTupleTypeNode(elementTypes: ReadonlyArray, idxIsSpread: boolean[] = []) { const node = createSynthesizedNode(SyntaxKind.TupleType) as TupleTypeNode; - node.elementTypes = createNodeArray(elementTypes); + const elements = map(elementTypes, (type: TypeNode, idx: number) => idxIsSpread[idx] ? createTypeSpread(type) : type); + node.elementTypes = createNodeArray(elements); return node; } @@ -2235,6 +2236,18 @@ namespace ts { : node; } + export function createTypeSpread(type: TypeNode) { + const node = createSynthesizedNode(SyntaxKind.TypeSpread); + node.type = type !== undefined ? parenthesizeElementTypeMember(type) : undefined; + return node; + } + + export function updateTypeSpread(node: TypeSpreadTypeNode, type: TypeNode) { + return node.type !== type + ? updateNode(createTypeSpread(type), node) + : node; + } + // Enum export function createEnumMember(name: string | PropertyName, initializer?: Expression) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 766d53cb433cf..2682d62c36a81 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -85,6 +85,8 @@ namespace ts { visitNode(cbNode, (node).objectAssignmentInitializer); case SyntaxKind.SpreadAssignment: return visitNode(cbNode, (node).expression); + case SyntaxKind.TypeSpread: + return visitNode(cbNode, (node).type); case SyntaxKind.Parameter: case SyntaxKind.PropertyDeclaration: case SyntaxKind.PropertySignature: @@ -1380,8 +1382,9 @@ namespace ts { case ParsingContext.Parameters: return isStartOfParameter(); case ParsingContext.TypeArguments: - case ParsingContext.TupleElementTypes: return token() === SyntaxKind.CommaToken || isStartOfType(); + case ParsingContext.TupleElementTypes: + return token() === SyntaxKind.CommaToken || token() === SyntaxKind.DotDotDotToken || isStartOfType(); case ParsingContext.HeritageClauses: return isHeritageClause(); case ParsingContext.ImportOrExportSpecifiers: @@ -2583,7 +2586,7 @@ namespace ts { function parseTupleType(): TupleTypeNode { const node = createNode(SyntaxKind.TupleType); - node.elementTypes = parseBracketedList(ParsingContext.TupleElementTypes, parseType, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken); + node.elementTypes = parseBracketedList(ParsingContext.TupleElementTypes, parseTupleElement, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken); return finishNode(node); } @@ -5157,6 +5160,19 @@ namespace ts { return finishNode(node); } + function parseTupleElement(): TypeSpreadTypeNode | TypeNode { + return (token() === SyntaxKind.DotDotDotToken) ? + parseTypeSpread() : + parseType(); + } + + function parseTypeSpread(): TypeSpreadTypeNode { + const node = createNode(SyntaxKind.TypeSpread); + parseExpected(SyntaxKind.DotDotDotToken); + node.type = parseTypeOperatorOrHigher(); + return finishNode(node); + } + function parseObjectBindingElement(): BindingElement { const node = createNode(SyntaxKind.BindingElement); node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5347d7caaf02c..2ea46b6418332 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -343,6 +343,7 @@ namespace ts { PropertyAssignment, ShorthandPropertyAssignment, SpreadAssignment, + TypeSpread, // Enum EnumMember, @@ -1007,7 +1008,12 @@ namespace ts { export interface TupleTypeNode extends TypeNode { kind: SyntaxKind.TupleType; - elementTypes: NodeArray; + elementTypes: NodeArray; + } + + export interface TypeSpreadTypeNode extends TypeNode { + kind: SyntaxKind.TypeSpread; + type: TypeNode; } export type UnionOrIntersectionTypeNode = UnionTypeNode | IntersectionTypeNode; @@ -3215,6 +3221,7 @@ namespace ts { NonPrimitive = 1 << 24, // intrinsic object type /* @internal */ JsxAttributes = 1 << 25, // Jsx attributes type + SpreadTuple = 1 << 26, // tuple types containing spreads /* @internal */ Nullable = Undefined | Null, @@ -3463,6 +3470,12 @@ namespace ts { constraint?: Type; } + // spread tuple types (TypeFlags.SpreadTuple) + export interface SpreadTupleType extends Type { + elements: Type[]; + idxIsSpread: boolean[]; + } + // keyof T types (TypeFlags.Index) export interface IndexType extends Type { type: TypeVariable | UnionOrIntersectionType; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f3b40c702b639..5c5a0283929a0 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4695,6 +4695,10 @@ namespace ts { return node.kind === SyntaxKind.SpreadAssignment; } + export function isTypeSpread(node: Node): node is TypeSpreadTypeNode { + return node.kind === SyntaxKind.TypeSpread; + } + // Enum export function isEnumMember(node: Node): node is EnumMember { diff --git a/src/compiler/visitor.ts b/src/compiler/visitor.ts index 7d46630e227d4..9aecee99c3eb4 100644 --- a/src/compiler/visitor.ts +++ b/src/compiler/visitor.ts @@ -872,6 +872,10 @@ namespace ts { return updateSpreadAssignment(node, visitNode((node).expression, visitor, isExpression)); + case SyntaxKind.TypeSpread: + return updateTypeSpread(node, + visitNode((node).type, visitor, isTypeNode)); + // Enum case SyntaxKind.EnumMember: return updateEnumMember(node, @@ -1394,6 +1398,10 @@ namespace ts { result = reduceNode((node).expression, cbNode, result); break; + case SyntaxKind.TypeSpread: + result = reduceNode((node).type, cbNode, result); + break; + // Enum case SyntaxKind.EnumMember: result = reduceNode((node).name, cbNode, result); diff --git a/tests/baselines/reference/tupleTypeSpread.js b/tests/baselines/reference/tupleTypeSpread.js new file mode 100644 index 0000000000000..d7751fd4f5db2 --- /dev/null +++ b/tests/baselines/reference/tupleTypeSpread.js @@ -0,0 +1,7 @@ +//// [tupleTypeSpread.ts] +type a = [1, ...[2]]; +type Combine = [Head, ...Tail]; +type b = Combine<1, [2, 3]>; + + +//// [tupleTypeSpread.js] diff --git a/tests/baselines/reference/tupleTypeSpread.symbols b/tests/baselines/reference/tupleTypeSpread.symbols new file mode 100644 index 0000000000000..303808d580702 --- /dev/null +++ b/tests/baselines/reference/tupleTypeSpread.symbols @@ -0,0 +1,15 @@ +=== tests/cases/compiler/tupleTypeSpread.ts === +type a = [1, ...[2]]; +>a : Symbol(a, Decl(tupleTypeSpread.ts, 0, 0)) + +type Combine = [Head, ...Tail]; +>Combine : Symbol(Combine, Decl(tupleTypeSpread.ts, 0, 21)) +>Head : Symbol(Head, Decl(tupleTypeSpread.ts, 1, 13)) +>Tail : Symbol(Tail, Decl(tupleTypeSpread.ts, 1, 18)) +>Head : Symbol(Head, Decl(tupleTypeSpread.ts, 1, 13)) +>Tail : Symbol(Tail, Decl(tupleTypeSpread.ts, 1, 18)) + +type b = Combine<1, [2, 3]>; +>b : Symbol(b, Decl(tupleTypeSpread.ts, 1, 57)) +>Combine : Symbol(Combine, Decl(tupleTypeSpread.ts, 0, 21)) + diff --git a/tests/baselines/reference/tupleTypeSpread.types b/tests/baselines/reference/tupleTypeSpread.types new file mode 100644 index 0000000000000..2c8d183b44aaf --- /dev/null +++ b/tests/baselines/reference/tupleTypeSpread.types @@ -0,0 +1,15 @@ +=== tests/cases/compiler/tupleTypeSpread.ts === +type a = [1, ...[2]]; +>a : [1, 2] + +type Combine = [Head, ...Tail]; +>Combine : [Head, ...Tail] +>Head : Head +>Tail : Tail +>Head : Head +>Tail : Tail + +type b = Combine<1, [2, 3]>; +>b : [1, 2, 3] +>Combine : [Head, ...Tail] + diff --git a/tests/baselines/reference/tupleTypeSpreadErrors.errors.txt b/tests/baselines/reference/tupleTypeSpreadErrors.errors.txt new file mode 100644 index 0000000000000..a6f973bb75c71 --- /dev/null +++ b/tests/baselines/reference/tupleTypeSpreadErrors.errors.txt @@ -0,0 +1,8 @@ +tests/cases/compiler/tupleTypeSpreadErrors.ts(1,32): error TS2714: Tuple type spreads may only be created from tuple types. + + +==== tests/cases/compiler/tupleTypeSpreadErrors.ts (1 errors) ==== + type Boom = [...T]; + ~~~~ +!!! error TS2714: Tuple type spreads may only be created from tuple types. + \ No newline at end of file diff --git a/tests/baselines/reference/tupleTypeSpreadErrors.js b/tests/baselines/reference/tupleTypeSpreadErrors.js new file mode 100644 index 0000000000000..51687e1669c4b --- /dev/null +++ b/tests/baselines/reference/tupleTypeSpreadErrors.js @@ -0,0 +1,5 @@ +//// [tupleTypeSpreadErrors.ts] +type Boom = [...T]; + + +//// [tupleTypeSpreadErrors.js] diff --git a/tests/cases/compiler/tupleTypeSpread.ts b/tests/cases/compiler/tupleTypeSpread.ts new file mode 100644 index 0000000000000..a2693a8f9f4af --- /dev/null +++ b/tests/cases/compiler/tupleTypeSpread.ts @@ -0,0 +1,4 @@ +// @allowSyntheticDefaultImports: true +type a = [1, ...[2]]; +type Combine = [Head, ...Tail]; +type b = Combine<1, [2, 3]>; diff --git a/tests/cases/compiler/tupleTypeSpreadErrors.ts b/tests/cases/compiler/tupleTypeSpreadErrors.ts new file mode 100644 index 0000000000000..0d96b3c1ad488 --- /dev/null +++ b/tests/cases/compiler/tupleTypeSpreadErrors.ts @@ -0,0 +1 @@ +type Boom = [...T];