From 57ca7680c9f5ec0d772c9dd94294cbcff2a29129 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 5 Dec 2017 14:18:11 -0800 Subject: [PATCH] Initial implementation of conditional type operator --- src/compiler/binder.ts | 1 + src/compiler/checker.ts | 52 ++++++++++++++++++++++++++++++ src/compiler/declarationEmitter.ts | 14 +++++++- src/compiler/emitter.ts | 12 +++++++ src/compiler/factory.ts | 24 +++++++++++++- src/compiler/parser.ts | 26 ++++++++++++++- src/compiler/transformers/ts.ts | 1 + src/compiler/types.ts | 37 +++++++++++++++------ src/compiler/utilities.ts | 4 +++ src/compiler/visitor.ts | 7 ++++ 10 files changed, 165 insertions(+), 13 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 71b5e1afbff..80c8b3dc315 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -3416,6 +3416,7 @@ namespace ts { case SyntaxKind.TupleType: case SyntaxKind.UnionType: case SyntaxKind.IntersectionType: + case SyntaxKind.ConditionalType: case SyntaxKind.ParenthesizedType: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.TypeAliasDeclaration: diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2ab67120e4b..cbf31179b92 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2620,6 +2620,13 @@ namespace ts { const indexTypeNode = typeToTypeNodeHelper((type).indexType, context); return createIndexedAccessTypeNode(objectTypeNode, indexTypeNode); } + if (type.flags & TypeFlags.Conditional) { + const checkTypeNode = typeToTypeNodeHelper((type).checkType, context); + const extendskTypeNode = typeToTypeNodeHelper((type).extendsType, context); + const trueTypeNode = typeToTypeNodeHelper((type).trueType, context); + const falseTypeNode = typeToTypeNodeHelper((type).falseType, context); + return createConditionalTypeNode(checkTypeNode, extendskTypeNode, trueTypeNode, falseTypeNode); + } Debug.fail("Should be unreachable."); @@ -3388,6 +3395,15 @@ namespace ts { writeType((type).indexType, TypeFormatFlags.None); writePunctuation(writer, SyntaxKind.CloseBracketToken); } + else if (type.flags & TypeFlags.Conditional) { + writeType((type).checkType, TypeFormatFlags.InElementType); + writer.writeKeyword("extends"); + writeType((type).extendsType, TypeFormatFlags.InElementType); + writePunctuation(writer, SyntaxKind.QuestionToken); + writeType((type).trueType, TypeFormatFlags.InElementType); + writePunctuation(writer, SyntaxKind.ColonToken); + writeType((type).falseType, TypeFormatFlags.InElementType); + } else { // Should never get here // { ... } @@ -8189,6 +8205,35 @@ namespace ts { return links.resolvedType; } + function getConditionalType(checkType: Type, extendsType: Type, trueType: Type, falseType: Type, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type { + if (checkType.flags & TypeFlags.Union) { + return getUnionType(map((checkType).types, t => getConditionalType(t, extendsType, trueType, falseType)), + /*subtypeReduction*/ false, aliasSymbol, aliasTypeArguments); + } + if (isTypeAssignableTo(checkType, extendsType)) { + return trueType; + } + if (!isGenericObjectType(checkType) && !isGenericObjectType(extendsType)) { + return falseType; + } + const type = createType(TypeFlags.Conditional); + type.checkType = checkType; + type.extendsType = extendsType; + type.trueType = trueType; + type.falseType = falseType; + return type; + } + + function getTypeFromConditionalTypeNode(node: ConditionalTypeNode): Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getConditionalType(getTypeFromTypeNode(node.checkType), getTypeFromTypeNode(node.extendsType), + getTypeFromTypeNode(node.trueType), getTypeFromTypeNode(node.falseType), + getAliasSymbolForTypeNode(node), getAliasTypeArgumentsForTypeNode(node)); + } + return links.resolvedType; + } + function getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node: TypeNode): Type { const links = getNodeLinks(node); if (!links.resolvedType) { @@ -8481,6 +8526,8 @@ namespace ts { return getTypeFromIndexedAccessTypeNode(node); case SyntaxKind.MappedType: return getTypeFromMappedTypeNode(node); + case SyntaxKind.ConditionalType: + return getTypeFromConditionalTypeNode(node); // This function assumes that an identifier or qualified name is a type expression // Callers should first ensure this by calling isTypeNode case SyntaxKind.Identifier: @@ -8778,6 +8825,11 @@ namespace ts { if (type.flags & TypeFlags.IndexedAccess) { return getIndexedAccessType(instantiateType((type).objectType, mapper), instantiateType((type).indexType, mapper)); } + if (type.flags & TypeFlags.Conditional) { + return getConditionalType(instantiateType((type).checkType, mapper), instantiateType((type).extendsType, mapper), + instantiateType((type).trueType, mapper), instantiateType((type).falseType, mapper), + type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)); + } } return type; } diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index dc76e0ddf50..4b494bf215e 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -450,6 +450,8 @@ namespace ts { return emitUnionType(type); case SyntaxKind.IntersectionType: return emitIntersectionType(type); + case SyntaxKind.ConditionalType: + return emitConditionalType(type); case SyntaxKind.ParenthesizedType: return emitParenType(type); case SyntaxKind.TypeOperator: @@ -545,7 +547,17 @@ namespace ts { emitSeparatedList(type.types, " & ", emitType); } - function emitParenType(type: ParenthesizedTypeNode) { + function emitConditionalType(node: ConditionalTypeNode) { + emitType(node.checkType); + write(" extends "); + emitType(node.extendsType); + write(" ? "); + emitType(node.trueType); + write(" : "); + emitType(node.falseType); + } + + function emitParenType(type: ParenthesizedTypeNode) { write("("); emitType(type.type); write(")"); diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 7a1d88d3bfd..10ecd3c8ffe 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -562,6 +562,8 @@ namespace ts { return emitUnionType(node); case SyntaxKind.IntersectionType: return emitIntersectionType(node); + case SyntaxKind.ConditionalType: + return emitConditionalType(node); case SyntaxKind.ParenthesizedType: return emitParenthesizedType(node); case SyntaxKind.ExpressionWithTypeArguments: @@ -1129,6 +1131,16 @@ namespace ts { emitList(node, node.types, ListFormat.IntersectionTypeConstituents); } + function emitConditionalType(node: ConditionalTypeNode) { + emit(node.checkType); + write(" extends "); + emit(node.extendsType); + write(" ? "); + emit(node.trueType); + write(" : "); + emit(node.falseType); + } + function emitParenthesizedType(node: ParenthesizedTypeNode) { write("("); emit(node.type); diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index dfc22e31833..070715d8ea3 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -720,6 +720,24 @@ namespace ts { : node; } + export function createConditionalTypeNode(checkType: TypeNode, extendsType: TypeNode, trueType: TypeNode, falseType: TypeNode) { + const node = createSynthesizedNode(SyntaxKind.ConditionalType) as ConditionalTypeNode; + node.checkType = parenthesizeConditionalTypeMember(checkType); + node.extendsType = parenthesizeConditionalTypeMember(extendsType); + node.trueType = trueType; + node.falseType = falseType; + return node; + } + + export function updateConditionalTypeNode(node: ConditionalTypeNode, checkType: TypeNode, extendsType: TypeNode, trueType: TypeNode, falseType: TypeNode) { + return node.checkType !== checkType + || node.extendsType !== extendsType + || node.trueType !== trueType + || node.falseType !== falseType + ? updateNode(createConditionalTypeNode(checkType, extendsType, trueType, falseType), node) + : node; + } + export function createParenthesizedType(type: TypeNode) { const node = createSynthesizedNode(SyntaxKind.ParenthesizedType); node.type = type; @@ -4081,6 +4099,10 @@ namespace ts { return expression; } + export function parenthesizeConditionalTypeMember(member: TypeNode) { + return member.kind === SyntaxKind.ConditionalType ? createParenthesizedType(member) : member; + } + export function parenthesizeElementTypeMember(member: TypeNode) { switch (member.kind) { case SyntaxKind.UnionType: @@ -4089,7 +4111,7 @@ namespace ts { case SyntaxKind.ConstructorType: return createParenthesizedType(member); } - return member; + return parenthesizeConditionalTypeMember(member); } export function parenthesizeArrayTypeMember(member: TypeNode) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index cb28be4677a..0d8aad97c28 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -147,6 +147,11 @@ namespace ts { case SyntaxKind.UnionType: case SyntaxKind.IntersectionType: return visitNodes(cbNode, cbNodes, (node).types); + case SyntaxKind.ConditionalType: + return visitNode(cbNode, (node).checkType) || + visitNode(cbNode, (node).extendsType) || + visitNode(cbNode, (node).trueType) || + visitNode(cbNode, (node).falseType); case SyntaxKind.ParenthesizedType: case SyntaxKind.TypeOperator: return visitNode(cbNode, (node).type); @@ -2760,6 +2765,10 @@ namespace ts { type = createJSDocPostfixType(SyntaxKind.JSDocNonNullableType, type); break; case SyntaxKind.QuestionToken: + // only parse postfix ? inside jsdoc, otherwise it is a conditional type + if (!(contextFlags & NodeFlags.JSDoc)) { + return type; + } type = createJSDocPostfixType(SyntaxKind.JSDocNullableType, type); break; case SyntaxKind.OpenBracketToken: @@ -2839,6 +2848,21 @@ namespace ts { return parseUnionOrIntersectionType(SyntaxKind.UnionType, parseIntersectionTypeOrHigher, SyntaxKind.BarToken); } + function parseConditionalTypeOrHigher(): TypeNode { + const type = parseUnionTypeOrHigher(); + if (parseOptional(SyntaxKind.ExtendsKeyword)) { + const node = createNode(SyntaxKind.ConditionalType, type.pos); + node.checkType = type; + node.extendsType = parseUnionTypeOrHigher(); + parseExpected(SyntaxKind.QuestionToken); + node.trueType = parseConditionalTypeOrHigher(); + parseExpected(SyntaxKind.ColonToken); + node.falseType = parseConditionalTypeOrHigher(); + return finishNode(node); + } + return type; + } + function isStartOfFunctionType(): boolean { if (token() === SyntaxKind.LessThanToken) { return true; @@ -2928,7 +2952,7 @@ namespace ts { if (token() === SyntaxKind.NewKeyword) { return parseFunctionOrConstructorType(SyntaxKind.ConstructorType); } - return parseUnionTypeOrHigher(); + return parseConditionalTypeOrHigher(); } function parseTypeAnnotation(): TypeNode { diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index 3c1bba280f0..20bc77c5e17 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -385,6 +385,7 @@ namespace ts { case SyntaxKind.TypeReference: case SyntaxKind.UnionType: case SyntaxKind.IntersectionType: + case SyntaxKind.ConditionalType: case SyntaxKind.ParenthesizedType: case SyntaxKind.ThisType: case SyntaxKind.TypeOperator: diff --git a/src/compiler/types.ts b/src/compiler/types.ts index af92654c0a5..f1b985f1544 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -248,6 +248,7 @@ namespace ts { TupleType, UnionType, IntersectionType, + ConditionalType, ParenthesizedType, ThisType, TypeOperator, @@ -1065,6 +1066,14 @@ namespace ts { types: NodeArray; } + export interface ConditionalTypeNode extends TypeNode { + kind: SyntaxKind.ConditionalType; + checkType: TypeNode; + extendsType: TypeNode; + trueType: TypeNode; + falseType: TypeNode; + } + export interface ParenthesizedTypeNode extends TypeNode { kind: SyntaxKind.ParenthesizedType; type: TypeNode; @@ -3339,18 +3348,19 @@ namespace ts { Intersection = 1 << 18, // Intersection (T & U) Index = 1 << 19, // keyof T IndexedAccess = 1 << 20, // T[K] + Conditional = 1 << 21, // A extends B ? T : U /* @internal */ - FreshLiteral = 1 << 21, // Fresh literal or unique type + FreshLiteral = 1 << 22, // Fresh literal or unique type /* @internal */ - ContainsWideningType = 1 << 22, // Type is or contains undefined or null widening type + ContainsWideningType = 1 << 23, // Type is or contains undefined or null widening type /* @internal */ - ContainsObjectLiteral = 1 << 23, // Type is or contains object literal type + ContainsObjectLiteral = 1 << 24, // Type is or contains object literal type /* @internal */ - ContainsAnyFunctionType = 1 << 24, // Type is or contains the anyFunctionType - NonPrimitive = 1 << 25, // intrinsic object type + ContainsAnyFunctionType = 1 << 25, // Type is or contains the anyFunctionType + NonPrimitive = 1 << 26, // intrinsic object type /* @internal */ - JsxAttributes = 1 << 26, // Jsx attributes type - MarkerType = 1 << 27, // Marker type used for variance probing + JsxAttributes = 1 << 27, // Jsx attributes type + MarkerType = 1 << 28, // Marker type used for variance probing /* @internal */ Nullable = Undefined | Null, @@ -3373,12 +3383,12 @@ namespace ts { ESSymbolLike = ESSymbol | UniqueESSymbol, UnionOrIntersection = Union | Intersection, StructuredType = Object | Union | Intersection, - StructuredOrTypeVariable = StructuredType | TypeParameter | Index | IndexedAccess, - TypeVariable = TypeParameter | IndexedAccess, + TypeVariable = TypeParameter | IndexedAccess | Conditional, + StructuredOrTypeVariable = StructuredType | TypeVariable | Index, // 'Narrowable' types are types where narrowing actually narrows. // This *should* be every type other than null, undefined, void, and never - Narrowable = Any | StructuredType | TypeParameter | Index | IndexedAccess | StringLike | NumberLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive, + Narrowable = Any | StructuredOrTypeVariable | StringLike | NumberLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive, NotUnionOrUnit = Any | ESSymbol | Object | NonPrimitive, /* @internal */ RequiresWidening = ContainsWideningType | ContainsObjectLiteral, @@ -3626,6 +3636,13 @@ namespace ts { type: TypeVariable | UnionOrIntersectionType; } + export interface ConditionalType extends TypeVariable { + checkType: Type; + extendsType: Type; + trueType: Type; + falseType: Type; + } + export const enum SignatureKind { Call, Construct, diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 0e62c2d8cea..25f16b9a89d 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4527,6 +4527,10 @@ namespace ts { return node.kind === SyntaxKind.IntersectionType; } + export function isConditionalTypeNode(node: Node): node is ConditionalTypeNode { + return node.kind === SyntaxKind.ConditionalType; + } + export function isParenthesizedTypeNode(node: Node): node is ParenthesizedTypeNode { return node.kind === SyntaxKind.ParenthesizedType; } diff --git a/src/compiler/visitor.ts b/src/compiler/visitor.ts index 0b40e20a7b7..b047a778015 100644 --- a/src/compiler/visitor.ts +++ b/src/compiler/visitor.ts @@ -385,6 +385,13 @@ namespace ts { return updateIntersectionTypeNode(node, nodesVisitor((node).types, visitor, isTypeNode)); + case SyntaxKind.ConditionalType: + return updateConditionalTypeNode(node, + visitNode((node).checkType, visitor, isTypeNode), + visitNode((node).extendsType, visitor, isTypeNode), + visitNode((node).trueType, visitor, isTypeNode), + visitNode((node).falseType, visitor, isTypeNode)); + case SyntaxKind.ParenthesizedType: return updateParenthesizedType(node, visitNode((node).type, visitor, isTypeNode));