From d6089414805ce30cc00c979e8075efaf615b56d1 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 25 Jan 2018 12:25:00 -0800 Subject: [PATCH] Implement type inference in conditional types --- src/compiler/binder.ts | 33 ++++++++++++- src/compiler/checker.ts | 77 +++++++++++++++++++++++++----- src/compiler/declarationEmitter.ts | 7 +++ src/compiler/emitter.ts | 7 +++ src/compiler/factory.ts | 12 +++++ src/compiler/parser.ts | 14 ++++++ src/compiler/scanner.ts | 1 + src/compiler/types.ts | 12 ++++- src/compiler/utilities.ts | 4 ++ src/compiler/visitor.ts | 4 ++ 10 files changed, 158 insertions(+), 13 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 9b96b78a4d3..d196d8add4d 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -101,6 +101,7 @@ namespace ts { HasLocals = 1 << 5, IsInterface = 1 << 6, IsObjectLiteralOrClassExpressionMethod = 1 << 7, + IsInferenceContainer = 1 << 8, } const binder = createBinder(); @@ -119,6 +120,7 @@ namespace ts { let parent: Node; let container: Node; let blockScopeContainer: Node; + let inferenceContainer: Node; let lastContainer: Node; let seenThisKeyword: boolean; @@ -186,6 +188,7 @@ namespace ts { parent = undefined; container = undefined; blockScopeContainer = undefined; + inferenceContainer = undefined; lastContainer = undefined; seenThisKeyword = false; currentFlow = undefined; @@ -561,6 +564,13 @@ namespace ts { bindChildren(node); node.flags = seenThisKeyword ? node.flags | NodeFlags.ContainsThis : node.flags & ~NodeFlags.ContainsThis; } + else if (containerFlags & ContainerFlags.IsInferenceContainer) { + const saveInferenceContainer = inferenceContainer; + inferenceContainer = node; + node.locals = undefined; + bindChildren(node); + inferenceContainer = saveInferenceContainer; + } else { bindChildren(node); } @@ -1417,6 +1427,9 @@ namespace ts { case SyntaxKind.MappedType: return ContainerFlags.IsContainer | ContainerFlags.HasLocals; + case SyntaxKind.ConditionalType: + return ContainerFlags.IsInferenceContainer; + case SyntaxKind.SourceFile: return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals; @@ -2059,7 +2072,7 @@ namespace ts { case SyntaxKind.TypePredicate: return checkTypePredicate(node as TypePredicateNode); case SyntaxKind.TypeParameter: - return declareSymbolAndAddToSymbolTable(node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + return bindTypeParameter(node as TypeParameterDeclaration); case SyntaxKind.Parameter: return bindParameter(node); case SyntaxKind.VariableDeclaration: @@ -2576,6 +2589,23 @@ namespace ts { : declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); } + function bindTypeParameter(node: TypeParameterDeclaration) { + if (node.parent.kind === SyntaxKind.InferType) { + if (inferenceContainer) { + if (!inferenceContainer.locals) { + inferenceContainer.locals = createSymbolTable(); + } + declareSymbol(inferenceContainer.locals, /*parent*/ undefined, node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + } + else { + bindAnonymousDeclaration(node, SymbolFlags.TypeParameter, getDeclarationName(node)); + } + } + else { + declareSymbolAndAddToSymbolTable(node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + } + } + // reachability checks function shouldReportErrorOnModuleDeclaration(node: ModuleDeclaration): boolean { @@ -3441,6 +3471,7 @@ namespace ts { case SyntaxKind.UnionType: case SyntaxKind.IntersectionType: case SyntaxKind.ConditionalType: + case SyntaxKind.InferType: case SyntaxKind.ParenthesizedType: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.TypeAliasDeclaration: diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6fe8f5d2e14..0786aee366a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1169,6 +1169,11 @@ namespace ts { ); } } + else if (location.kind === SyntaxKind.ConditionalType) { + // A type parameter declared using 'infer T' in a conditional type is visible only in + // the true branch of the conditional type. + useResult = lastLocation === (location).trueType; + } if (useResult) { break loop; @@ -4628,10 +4633,14 @@ namespace ts { case SyntaxKind.TypeAliasDeclaration: case SyntaxKind.JSDocTemplateTag: case SyntaxKind.MappedType: + case SyntaxKind.ConditionalType: const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes); if (node.kind === SyntaxKind.MappedType) { return append(outerTypeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode((node).typeParameter))); } + else if (node.kind === SyntaxKind.ConditionalType) { + return concatenate(outerTypeParameters, getInferTypeParameters(node)); + } const outerAndOwnTypeParameters = appendTypeParameters(outerTypeParameters, getEffectiveTypeParameterDeclarations(node) || emptyArray); const thisType = includeThisTypes && (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression || node.kind === SyntaxKind.InterfaceDeclaration) && @@ -8078,12 +8087,13 @@ namespace ts { return type.flags & TypeFlags.Substitution ? (type).typeParameter : type; } - function createConditionalType(checkType: Type, extendsType: Type, trueType: Type, falseType: Type, target: ConditionalType, mapper: TypeMapper, aliasSymbol: Symbol, aliasTypeArguments: Type[]) { + function createConditionalType(checkType: Type, extendsType: Type, trueType: Type, falseType: Type, inferTypeParameters: TypeParameter[], target: ConditionalType, mapper: TypeMapper, aliasSymbol: Symbol, aliasTypeArguments: Type[]) { const type = createType(TypeFlags.Conditional); type.checkType = checkType; type.extendsType = extendsType; type.trueType = trueType; type.falseType = falseType; + type.inferTypeParameters = inferTypeParameters; type.target = target; type.mapper = mapper; type.aliasSymbol = aliasSymbol; @@ -8091,14 +8101,29 @@ namespace ts { return type; } - function getConditionalType(checkType: Type, extendsType: Type, baseTrueType: Type, baseFalseType: Type, target: ConditionalType, mapper: TypeMapper, aliasSymbol?: Symbol, baseAliasTypeArguments?: Type[]): Type { + function getConditionalType(checkType: Type, baseExtendsType: Type, baseTrueType: Type, baseFalseType: Type, inferTypeParameters: TypeParameter[], target: ConditionalType, mapper: TypeMapper, aliasSymbol?: Symbol, baseAliasTypeArguments?: Type[]): Type { + // Instantiate extends type without instantiating any 'infer T' type parameters + const extendsType = instantiateType(baseExtendsType, mapper); + let combinedMapper: TypeMapper; + if (inferTypeParameters) { + const inferences = map(inferTypeParameters, createInferenceInfo); + // We don't want inferences from constraints as they may cause us to eagerly resolve the + // conditional type instead of deferring resolution. + inferTypes(inferences, checkType, extendsType, InferencePriority.NoConstraints); + // We infer 'never' when there are no candidates for a type parameter + const inferredTypes = map(inferences, inference => getTypeFromInference(inference) || neverType); + const inferenceMapper = createTypeMapper(inferTypeParameters, inferredTypes); + combinedMapper = mapper ? combineTypeMappers(mapper, inferenceMapper) : inferenceMapper; + } // Return union of trueType and falseType for any and never since they match anything if (checkType.flags & (TypeFlags.Any | TypeFlags.Never)) { - return getUnionType([instantiateType(baseTrueType, mapper), instantiateType(baseFalseType, mapper)]); + return getUnionType([instantiateType(baseTrueType, combinedMapper || mapper), instantiateType(baseFalseType, mapper)]); } + // Instantiate the extends type including inferences for 'infer T' type parameters + const inferredExtendsType = combinedMapper ? instantiateType(baseExtendsType, combinedMapper) : extendsType; // Return trueType for a definitely true extends check - if (isTypeAssignableTo(checkType, extendsType)) { - return instantiateType(baseTrueType, mapper); + if (isTypeAssignableTo(checkType, inferredExtendsType)) { + return instantiateType(baseTrueType, combinedMapper || mapper); } // Return falseType for a definitely false extends check if (!isTypeAssignableTo(instantiateType(checkType, anyMapper), instantiateType(extendsType, constraintMapper))) { @@ -8114,25 +8139,45 @@ namespace ts { return cached; } const result = createConditionalType(erasedCheckType, extendsType, trueType, falseType, - target, mapper, aliasSymbol, instantiateTypes(baseAliasTypeArguments, mapper)); + inferTypeParameters, target, mapper, aliasSymbol, instantiateTypes(baseAliasTypeArguments, mapper)); if (id) { conditionalTypes.set(id, result); } return result; } + function getInferTypeParameters(node: ConditionalTypeNode): TypeParameter[] { + let result: TypeParameter[]; + if (node.locals) { + node.locals.forEach(symbol => { + if (symbol.flags & SymbolFlags.TypeParameter) { + result = append(result, getDeclaredTypeOfSymbol(symbol)); + } + }); + } + return result; + } + 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), - /*target*/ undefined, /*mapper*/ undefined, + getInferTypeParameters(node), /*target*/ undefined, /*mapper*/ undefined, getAliasSymbolForTypeNode(node), getAliasTypeArgumentsForTypeNode(node)); } return links.resolvedType; } + function getTypeFromInferTypeNode(node: InferTypeNode): Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node.typeParameter)); + } + return links.resolvedType; + } + function getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node: TypeNode): Type { const links = getNodeLinks(node); if (!links.resolvedType) { @@ -8423,6 +8468,8 @@ namespace ts { return getTypeFromMappedTypeNode(node); case SyntaxKind.ConditionalType: return getTypeFromConditionalTypeNode(node); + case SyntaxKind.InferType: + return getTypeFromInferTypeNode(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: @@ -8714,8 +8761,8 @@ namespace ts { } function instantiateConditionalType(type: ConditionalType, mapper: TypeMapper): Type { - return getConditionalType(instantiateType(type.checkType, mapper), instantiateType(type.extendsType, mapper), - type.trueType, type.falseType, type, mapper, type.aliasSymbol, type.aliasTypeArguments); + return getConditionalType(instantiateType(type.checkType, mapper), type.extendsType, type.trueType, type.falseType, + type.inferTypeParameters, type, mapper, type.aliasSymbol, type.aliasTypeArguments); } function instantiateType(type: Type, mapper: TypeMapper): Type { @@ -11206,7 +11253,7 @@ namespace ts { const templateType = getTemplateTypeFromMappedType(target); const inference = createInferenceInfo(typeParameter); inferTypes([inference], sourceType, templateType); - return inference.candidates ? getUnionType(inference.candidates, UnionReduction.Subtype) : emptyObjectType; + return getTypeFromInference(inference) || emptyObjectType; } function getUnmatchedProperty(source: Type, target: Type, requireOptionalProperties: boolean) { @@ -11222,6 +11269,12 @@ namespace ts { return undefined; } + function getTypeFromInference(inference: InferenceInfo) { + return inference.candidates ? getUnionType(inference.candidates, UnionReduction.Subtype) : + inference.contraCandidates ? getCommonSubtype(inference.contraCandidates) : + undefined; + } + function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0) { let symbolStack: Symbol[]; let visited: Map; @@ -11381,7 +11434,9 @@ namespace ts { } } else { - source = getApparentType(source); + if (!(priority && InferencePriority.NoConstraints && source.flags & (TypeFlags.Intersection | TypeFlags.Instantiable))) { + source = getApparentType(source); + } if (source.flags & (TypeFlags.Object | TypeFlags.Intersection)) { const key = source.id + "," + target.id; if (visited && visited.get(key)) { diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index 1e69ce748fa..794cb4ff2b3 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -452,6 +452,8 @@ namespace ts { return emitIntersectionType(type); case SyntaxKind.ConditionalType: return emitConditionalType(type); + case SyntaxKind.InferType: + return emitInferType(type); case SyntaxKind.ParenthesizedType: return emitParenType(type); case SyntaxKind.TypeOperator: @@ -557,6 +559,11 @@ namespace ts { emitType(node.falseType); } + function emitInferType(node: InferTypeNode) { + write("infer "); + writeTextOfNode(currentText, node.typeParameter.name); + } + function emitParenType(type: ParenthesizedTypeNode) { write("("); emitType(type.type); diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index eac1a35164b..9ba2c880e22 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -602,6 +602,8 @@ namespace ts { return emitIntersectionType(node); case SyntaxKind.ConditionalType: return emitConditionalType(node); + case SyntaxKind.InferType: + return emitInferType(node); case SyntaxKind.ParenthesizedType: return emitParenthesizedType(node); case SyntaxKind.ExpressionWithTypeArguments: @@ -1202,6 +1204,11 @@ namespace ts { emit(node.falseType); } + function emitInferType(node: InferTypeNode) { + write("infer "); + emit(node.typeParameter); + } + function emitParenthesizedType(node: ParenthesizedTypeNode) { writePunctuation("("); emit(node.type); diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index f9e61249f77..5a52b9e2d8d 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -747,6 +747,18 @@ namespace ts { : node; } + export function createInferTypeNode(typeParameter: TypeParameterDeclaration) { + const node = createSynthesizedNode(SyntaxKind.InferType); + node.typeParameter = typeParameter; + return node; + } + + export function updateInferTypeNode(node: InferTypeNode, typeParameter: TypeParameterDeclaration) { + return node.typeParameter !== typeParameter + ? updateNode(createInferTypeNode(typeParameter), node) + : node; + } + export function createParenthesizedType(type: TypeNode) { const node = createSynthesizedNode(SyntaxKind.ParenthesizedType); node.type = type; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 733bcd02040..baf7d7d0999 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -180,6 +180,8 @@ namespace ts { visitNode(cbNode, (node).extendsType) || visitNode(cbNode, (node).trueType) || visitNode(cbNode, (node).falseType); + case SyntaxKind.InferType: + return visitNode(cbNode, (node).typeParameter); case SyntaxKind.ParenthesizedType: case SyntaxKind.TypeOperator: return visitNode(cbNode, (node).type); @@ -2647,6 +2649,15 @@ namespace ts { return finishNode(node); } + function parseInferType(): InferTypeNode { + const node = createNode(SyntaxKind.InferType); + parseExpected(SyntaxKind.InferKeyword); + const typeParameter = createNode(SyntaxKind.TypeParameter); + typeParameter.name = parseIdentifier(); + node.typeParameter = finishNode(typeParameter); + return finishNode(node); + } + function parseFunctionOrConstructorType(kind: SyntaxKind): FunctionOrConstructorTypeNode { const node = createNodeWithJSDoc(kind); if (kind === SyntaxKind.ConstructorType) { @@ -2733,6 +2744,8 @@ namespace ts { return parseTupleType(); case SyntaxKind.OpenParenToken: return parseParenthesizedType(); + case SyntaxKind.InferKeyword: + return parseInferType(); default: return parseTypeReference(); } @@ -2767,6 +2780,7 @@ namespace ts { case SyntaxKind.QuestionToken: case SyntaxKind.ExclamationToken: case SyntaxKind.DotDotDotToken: + case SyntaxKind.InferKeyword: return true; case SyntaxKind.MinusToken: return !inStartOfParameter && lookAhead(nextTokenIsNumericLiteral); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 5001ea58336..14e27a54ec0 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -92,6 +92,7 @@ namespace ts { "implements": SyntaxKind.ImplementsKeyword, "import": SyntaxKind.ImportKeyword, "in": SyntaxKind.InKeyword, + "infer": SyntaxKind.InferKeyword, "instanceof": SyntaxKind.InstanceOfKeyword, "interface": SyntaxKind.InterfaceKeyword, "is": SyntaxKind.IsKeyword, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 13c9f151fec..b40c0ad849e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -215,6 +215,7 @@ namespace ts { ConstructorKeyword, DeclareKeyword, GetKeyword, + InferKeyword, IsKeyword, KeyOfKeyword, ModuleKeyword, @@ -266,6 +267,7 @@ namespace ts { UnionType, IntersectionType, ConditionalType, + InferType, ParenthesizedType, ThisType, TypeOperator, @@ -771,7 +773,7 @@ namespace ts { export interface TypeParameterDeclaration extends NamedDeclaration { kind: SyntaxKind.TypeParameter; - parent?: DeclarationWithTypeParameters; + parent?: DeclarationWithTypeParameters | InferTypeNode; name: Identifier; constraint?: TypeNode; default?: TypeNode; @@ -1125,6 +1127,11 @@ namespace ts { falseType: TypeNode; } + export interface InferTypeNode extends TypeNode { + kind: SyntaxKind.InferType; + typeParameter: TypeParameterDeclaration; + } + export interface ParenthesizedTypeNode extends TypeNode { kind: SyntaxKind.ParenthesizedType; type: TypeNode; @@ -3795,6 +3802,8 @@ namespace ts { trueType: Type; falseType: Type; /* @internal */ + inferTypeParameters: TypeParameter[]; + /* @internal */ target?: ConditionalType; /* @internal */ mapper?: TypeMapper; @@ -3870,6 +3879,7 @@ namespace ts { NakedTypeVariable = 1 << 0, // Naked type variable in union or intersection type MappedType = 1 << 1, // Reverse inference for mapped type ReturnType = 1 << 2, // Inference made from return type of generic function + NoConstraints = 1 << 3, // Don't infer from constraints of instantiable types } export interface InferenceInfo { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 7fd142d981a..6362370ca03 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4559,6 +4559,10 @@ namespace ts { return node.kind === SyntaxKind.ConditionalType; } + export function isInferTypeNode(node: Node): node is InferTypeNode { + return node.kind === SyntaxKind.InferType; + } + 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 db37749caea..9c1462e7ec6 100644 --- a/src/compiler/visitor.ts +++ b/src/compiler/visitor.ts @@ -394,6 +394,10 @@ namespace ts { visitNode((node).trueType, visitor, isTypeNode), visitNode((node).falseType, visitor, isTypeNode)); + case SyntaxKind.InferType: + return updateInferTypeNode(node, + visitNode((node).typeParameter, visitor, isTypeParameterDeclaration)); + case SyntaxKind.ParenthesizedType: return updateParenthesizedType(node, visitNode((node).type, visitor, isTypeNode));