Merge pull request #16072 from Microsoft/improveTypeArgumentInference

Infer from generic function return types
This commit is contained in:
Anders Hejlsberg
2017-05-26 10:59:40 -07:00
committed by GitHub
12 changed files with 1803 additions and 174 deletions

View File

@@ -7953,23 +7953,14 @@ namespace ts {
return mapper;
}
function getInferenceMapper(context: InferenceContext): TypeMapper {
if (!context.mapper) {
const mapper: TypeMapper = t => {
const typeParameters = context.signature.typeParameters;
for (let i = 0; i < typeParameters.length; i++) {
if (t === typeParameters[i]) {
context.inferences[i].isFixed = true;
return getInferredType(context, i);
}
}
return t;
};
mapper.mappedTypes = context.signature.typeParameters;
mapper.context = context;
context.mapper = mapper;
}
return context.mapper;
function isInferenceContext(mapper: TypeMapper): mapper is InferenceContext {
return !!(<InferenceContext>mapper).signature;
}
function cloneTypeMapper(mapper: TypeMapper): TypeMapper {
return mapper && isInferenceContext(mapper) ?
createInferenceContext(mapper.signature, mapper.flags | InferenceFlags.NoDefault, mapper.inferences) :
mapper;
}
function identityMapper(type: Type): Type {
@@ -10165,23 +10156,45 @@ namespace ts {
}
}
function createInferenceContext(signature: Signature, inferUnionTypes: boolean, useAnyForNoInferences: boolean): InferenceContext {
const inferences = map(signature.typeParameters, createTypeInferencesObject);
function createInferenceContext(signature: Signature, flags: InferenceFlags, baseInferences?: InferenceInfo[]): InferenceContext {
const inferences = baseInferences ? map(baseInferences, cloneInferenceInfo) : map(signature.typeParameters, createInferenceInfo);
const context = mapper as InferenceContext;
context.mappedTypes = signature.typeParameters;
context.signature = signature;
context.inferences = inferences;
context.flags = flags;
return context;
function mapper(t: Type): Type {
for (let i = 0; i < inferences.length; i++) {
if (t === inferences[i].typeParameter) {
inferences[i].isFixed = true;
return getInferredType(context, i);
}
}
return t;
}
}
function createInferenceInfo(typeParameter: TypeParameter): InferenceInfo {
return {
signature,
inferUnionTypes,
inferences,
inferredTypes: new Array(signature.typeParameters.length),
useAnyForNoInferences
typeParameter,
candidates: undefined,
inferredType: undefined,
priority: undefined,
topLevel: true,
isFixed: false
};
}
function createTypeInferencesObject(): TypeInferences {
function cloneInferenceInfo(inference: InferenceInfo): InferenceInfo {
return {
primary: undefined,
secondary: undefined,
topLevel: true,
isFixed: false,
typeParameter: inference.typeParameter,
candidates: inference.candidates && inference.candidates.slice(),
inferredType: inference.inferredType,
priority: inference.priority,
topLevel: inference.topLevel,
isFixed: inference.isFixed
};
}
@@ -10218,10 +10231,9 @@ namespace ts {
if (properties.length === 0 && !indexInfo) {
return undefined;
}
const typeVariable = <TypeVariable>getIndexedAccessType((<IndexType>getConstraintTypeFromMappedType(target)).type, getTypeParameterFromMappedType(target));
const typeVariableArray = [typeVariable];
const typeInferences = createTypeInferencesObject();
const typeInferencesArray = [typeInferences];
const typeParameter = <TypeParameter>getIndexedAccessType((<IndexType>getConstraintTypeFromMappedType(target)).type, getTypeParameterFromMappedType(target));
const inference = createInferenceInfo(typeParameter);
const inferences = [inference];
const templateType = getTemplateTypeFromMappedType(target);
const readonlyMask = target.declaration.readonlyToken ? false : true;
const optionalMask = target.declaration.questionToken ? 0 : SymbolFlags.Optional;
@@ -10247,22 +10259,15 @@ namespace ts {
return createAnonymousType(undefined, members, emptyArray, emptyArray, indexInfo, undefined);
function inferTargetType(sourceType: Type): Type {
typeInferences.primary = undefined;
typeInferences.secondary = undefined;
inferTypes(typeVariableArray, typeInferencesArray, sourceType, templateType);
const inferences = typeInferences.primary || typeInferences.secondary;
return inferences && getUnionType(inferences, /*subtypeReduction*/ true);
inference.candidates = undefined;
inferTypes(inferences, sourceType, templateType);
return inference.candidates && getUnionType(inference.candidates, /*subtypeReduction*/ true);
}
}
function inferTypesWithContext(context: InferenceContext, originalSource: Type, originalTarget: Type) {
inferTypes(context.signature.typeParameters, context.inferences, originalSource, originalTarget);
}
function inferTypes(typeVariables: TypeVariable[], typeInferences: TypeInferences[], originalSource: Type, originalTarget: Type) {
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0) {
let symbolStack: Symbol[];
let visited: Map<boolean>;
let inferiority = 0;
inferFromTypes(originalSource, originalTarget);
function inferFromTypes(source: Type, target: Type) {
@@ -10322,32 +10327,26 @@ namespace ts {
// Because the anyFunctionType is internal, it should not be exposed to the user by adding
// it as an inference candidate. Hopefully, a better candidate will come along that does
// not contain anyFunctionType when we come back to this argument for its second round
// of inference.
if (source.flags & TypeFlags.ContainsAnyFunctionType) {
// of inference. Also, we exclude inferences for silentNeverType which is used as a wildcard
// when constructing types from type parameters that had no inference candidates.
if (source.flags & TypeFlags.ContainsAnyFunctionType || source === silentNeverType) {
return;
}
for (let i = 0; i < typeVariables.length; i++) {
if (target === typeVariables[i]) {
const inferences = typeInferences[i];
if (!inferences.isFixed) {
// Any inferences that are made to a type parameter in a union type are inferior
// to inferences made to a flat (non-union) type. This is because if we infer to
// T | string[], we really don't know if we should be inferring to T or not (because
// the correct constituent on the target side could be string[]). Therefore, we put
// such inferior inferences into a secondary bucket, and only use them if the primary
// bucket is empty.
const candidates = inferiority ?
inferences.secondary || (inferences.secondary = []) :
inferences.primary || (inferences.primary = []);
if (!contains(candidates, source)) {
candidates.push(source);
}
if (target.flags & TypeFlags.TypeParameter && !isTypeParameterAtTopLevel(originalTarget, <TypeParameter>target)) {
inferences.topLevel = false;
}
const inference = getInferenceInfoForType(target);
if (inference) {
if (!inference.isFixed) {
if (!inference.candidates || priority < inference.priority) {
inference.candidates = [source];
inference.priority = priority;
}
else if (priority === inference.priority) {
inference.candidates.push(source);
}
if (!(priority & InferencePriority.ReturnType) && target.flags & TypeFlags.TypeParameter && !isTypeParameterAtTopLevel(originalTarget, <TypeParameter>target)) {
inference.topLevel = false;
}
return;
}
return;
}
}
else if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (<TypeReference>source).target === (<TypeReference>target).target) {
@@ -10365,7 +10364,7 @@ namespace ts {
let typeVariable: TypeVariable;
// First infer to each type in union or intersection that isn't a type variable
for (const t of targetTypes) {
if (t.flags & TypeFlags.TypeVariable && contains(typeVariables, t)) {
if (getInferenceInfoForType(t)) {
typeVariable = <TypeVariable>t;
typeVariableCount++;
}
@@ -10377,9 +10376,10 @@ namespace ts {
// variable. This gives meaningful results for union types in co-variant positions and intersection
// types in contra-variant positions (such as callback parameters).
if (typeVariableCount === 1) {
inferiority++;
const savePriority = priority;
priority |= InferencePriority.NakedTypeVariable;
inferFromTypes(source, typeVariable);
inferiority--;
priority = savePriority;
}
}
else if (source.flags & TypeFlags.UnionOrIntersection) {
@@ -10419,6 +10419,17 @@ namespace ts {
}
}
function getInferenceInfoForType(type: Type) {
if (type.flags & TypeFlags.TypeVariable) {
for (const inference of inferences) {
if (type === inference.typeParameter) {
return inference;
}
}
}
return undefined;
}
function inferFromObjectTypes(source: Type, target: Type) {
if (getObjectFlags(target) & ObjectFlags.Mapped) {
const constraintType = getConstraintTypeFromMappedType(<MappedType>target);
@@ -10427,13 +10438,14 @@ namespace ts {
// where T is a type variable. Use inferTypeForHomomorphicMappedType to infer a suitable source
// type and then make a secondary inference from that type to T. We make a secondary inference
// such that direct inferences to T get priority over inferences to Partial<T>, for example.
const index = indexOf(typeVariables, (<IndexType>constraintType).type);
if (index >= 0 && !typeInferences[index].isFixed) {
const inference = getInferenceInfoForType((<IndexType>constraintType).type);
if (inference && !inference.isFixed) {
const inferredType = inferTypeForHomomorphicMappedType(source, <MappedType>target);
if (inferredType) {
inferiority++;
inferFromTypes(inferredType, typeVariables[index]);
inferiority--;
const savePriority = priority;
priority |= InferencePriority.MappedType;
inferFromTypes(inferredType, inference.typeParameter);
priority = savePriority;
}
}
return;
@@ -10532,66 +10544,68 @@ namespace ts {
return type.flags & TypeFlags.Union ? getUnionType(reducedTypes) : getIntersectionType(reducedTypes);
}
function getInferenceCandidates(context: InferenceContext, index: number): Type[] {
const inferences = context.inferences[index];
return inferences.primary || inferences.secondary || emptyArray;
}
function hasPrimitiveConstraint(type: TypeParameter): boolean {
const constraint = getConstraintOfTypeParameter(type);
return constraint && maybeTypeOfKind(constraint, TypeFlags.Primitive | TypeFlags.Index);
}
function getInferredType(context: InferenceContext, index: number): Type {
let inferredType = context.inferredTypes[index];
const inference = context.inferences[index];
let inferredType = inference.inferredType;
let inferenceSucceeded: boolean;
if (!inferredType) {
const inferences = getInferenceCandidates(context, index);
if (inferences.length) {
if (inference.candidates) {
// We widen inferred literal types if
// all inferences were made to top-level ocurrences of the type parameter, and
// the type parameter has no constraint or its constraint includes no primitive or literal types, and
// the type parameter was fixed during inference or does not occur at top-level in the return type.
const signature = context.signature;
const widenLiteralTypes = context.inferences[index].topLevel &&
!hasPrimitiveConstraint(signature.typeParameters[index]) &&
(context.inferences[index].isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), signature.typeParameters[index]));
const baseInferences = widenLiteralTypes ? sameMap(inferences, getWidenedLiteralType) : inferences;
// Infer widened union or supertype, or the unknown type for no common supertype
const unionOrSuperType = context.inferUnionTypes ? getUnionType(baseInferences, /*subtypeReduction*/ true) : getCommonSupertype(baseInferences);
const widenLiteralTypes = inference.topLevel &&
!hasPrimitiveConstraint(inference.typeParameter) &&
(inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter));
const baseCandidates = widenLiteralTypes ? sameMap(inference.candidates, getWidenedLiteralType) : inference.candidates;
// Infer widened union or supertype, or the unknown type for no common supertype. We infer union types
// for inferences coming from return types in order to avoid common supertype failures.
const unionOrSuperType = context.flags & InferenceFlags.InferUnionTypes || inference.priority & InferencePriority.ReturnType ?
getUnionType(baseCandidates, /*subtypeReduction*/ true) : getCommonSupertype(baseCandidates);
inferredType = unionOrSuperType ? getWidenedType(unionOrSuperType) : unknownType;
inferenceSucceeded = !!unionOrSuperType;
}
else {
// Infer either the default or the empty object type when no inferences were
// made. It is important to remember that in this case, inference still
// succeeds, meaning there is no error for not having inference candidates. An
// inference error only occurs when there are *conflicting* candidates, i.e.
// candidates with no common supertype.
const defaultType = getDefaultFromTypeParameter(context.signature.typeParameters[index]);
if (defaultType) {
// Instantiate the default type. Any forward reference to a type
// parameter should be instantiated to the empty object type.
inferredType = instantiateType(defaultType,
combineTypeMappers(
createBackreferenceMapper(context.signature.typeParameters, index),
getInferenceMapper(context)));
if (context.flags & InferenceFlags.NoDefault) {
// We use silentNeverType as the wildcard that signals no inferences.
inferredType = silentNeverType;
}
else {
inferredType = context.useAnyForNoInferences ? anyType : emptyObjectType;
// Infer either the default or the empty object type when no inferences were
// made. It is important to remember that in this case, inference still
// succeeds, meaning there is no error for not having inference candidates. An
// inference error only occurs when there are *conflicting* candidates, i.e.
// candidates with no common supertype.
const defaultType = getDefaultFromTypeParameter(inference.typeParameter);
if (defaultType) {
// Instantiate the default type. Any forward reference to a type
// parameter should be instantiated to the empty object type.
inferredType = instantiateType(defaultType,
combineTypeMappers(
createBackreferenceMapper(context.signature.typeParameters, index),
context));
}
else {
inferredType = context.flags & InferenceFlags.AnyDefault ? anyType : emptyObjectType;
}
}
inferenceSucceeded = true;
}
context.inferredTypes[index] = inferredType;
inference.inferredType = inferredType;
// Only do the constraint check if inference succeeded (to prevent cascading errors)
if (inferenceSucceeded) {
const constraint = getConstraintOfTypeParameter(context.signature.typeParameters[index]);
if (constraint) {
const instantiatedConstraint = instantiateType(constraint, getInferenceMapper(context));
const instantiatedConstraint = instantiateType(constraint, context);
if (!isTypeAssignableTo(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) {
context.inferredTypes[index] = inferredType = instantiatedConstraint;
inference.inferredType = inferredType = instantiatedConstraint;
}
}
}
@@ -10606,10 +10620,11 @@ namespace ts {
}
function getInferredTypes(context: InferenceContext): Type[] {
for (let i = 0; i < context.inferredTypes.length; i++) {
getInferredType(context, i);
const result: Type[] = [];
for (let i = 0; i < context.inferences.length; i++) {
result.push(getInferredType(context, i));
}
return context.inferredTypes;
return result;
}
// EXPRESSION TYPE CHECKING
@@ -14865,26 +14880,25 @@ namespace ts {
// Instantiate a generic signature in the context of a non-generic signature (section 3.8.5 in TypeScript spec)
function instantiateSignatureInContextOf(signature: Signature, contextualSignature: Signature, contextualMapper: TypeMapper): Signature {
const context = createInferenceContext(signature, /*inferUnionTypes*/ true, /*useAnyForNoInferences*/ false);
const context = createInferenceContext(signature, InferenceFlags.InferUnionTypes);
forEachMatchingParameterType(contextualSignature, signature, (source, target) => {
// Type parameters from outer context referenced by source type are fixed by instantiation of the source type
inferTypesWithContext(context, instantiateType(source, contextualMapper), target);
inferTypes(context.inferences, instantiateType(source, contextualMapper), target);
});
return getSignatureInstantiation(signature, getInferredTypes(context));
}
function inferTypeArguments(node: CallLikeExpression, signature: Signature, args: Expression[], excludeArgument: boolean[], context: InferenceContext): void {
const typeParameters = signature.typeParameters;
const inferenceMapper = getInferenceMapper(context);
function inferTypeArguments(node: CallLikeExpression, signature: Signature, args: Expression[], excludeArgument: boolean[], context: InferenceContext): Type[] {
const inferences = context.inferences;
// Clear out all the inference results from the last time inferTypeArguments was called on this context
for (let i = 0; i < typeParameters.length; i++) {
for (let i = 0; i < inferences.length; i++) {
// As an optimization, we don't have to clear (and later recompute) inferred types
// for type parameters that have already been fixed on the previous call to inferTypeArguments.
// It would be just as correct to reset all of them. But then we'd be repeating the same work
// for the type parameters that were fixed, namely the work done by getInferredType.
if (!context.inferences[i].isFixed) {
context.inferredTypes[i] = undefined;
if (!inferences[i].isFixed) {
inferences[i].inferredType = undefined;
}
}
@@ -14899,11 +14913,29 @@ namespace ts {
context.failedTypeParameterIndex = undefined;
}
// If a contextual type is available, infer from that type to the return type of the call expression. For
// example, given a 'function wrap<T, U>(cb: (x: T) => U): (x: T) => U' and a call expression
// 'let f: (x: string) => number = wrap(s => s.length)', we infer from the declared type of 'f' to the
// return type of 'wrap'.
if (isExpression(node)) {
const contextualType = getContextualType(node);
if (contextualType) {
// We clone the contextual mapper to avoid disturbing a resolution in progress for an
// outer call expression. Effectively we just want a snapshot of whatever has been
// inferred for any outer call expression so far.
const mapper = cloneTypeMapper(getContextualMapper(node));
const instantiatedType = instantiateType(contextualType, mapper);
const returnType = getReturnTypeOfSignature(signature);
// Inferences made from return types have lower priority than all other inferences.
inferTypes(context.inferences, instantiatedType, returnType, InferencePriority.ReturnType);
}
}
const thisType = getThisTypeOfSignature(signature);
if (thisType) {
const thisArgumentNode = getThisArgumentOfCall(node);
const thisArgumentType = thisArgumentNode ? checkExpression(thisArgumentNode) : voidType;
inferTypesWithContext(context, thisArgumentType, thisType);
inferTypes(context.inferences, thisArgumentType, thisType);
}
// We perform two passes over the arguments. In the first pass we infer from all arguments, but use
@@ -14921,11 +14953,11 @@ namespace ts {
if (argType === undefined) {
// For context sensitive arguments we pass the identityMapper, which is a signal to treat all
// context sensitive function expressions as wildcards
const mapper = excludeArgument && excludeArgument[i] !== undefined ? identityMapper : inferenceMapper;
const mapper = excludeArgument && excludeArgument[i] !== undefined ? identityMapper : context;
argType = checkExpressionWithContextualType(arg, paramType, mapper);
}
inferTypesWithContext(context, argType, paramType);
inferTypes(context.inferences, argType, paramType);
}
}
@@ -14940,12 +14972,11 @@ namespace ts {
if (excludeArgument[i] === false) {
const arg = args[i];
const paramType = getTypeAtPosition(signature, i);
inferTypesWithContext(context, checkExpressionWithContextualType(arg, paramType, inferenceMapper), paramType);
inferTypes(context.inferences, checkExpressionWithContextualType(arg, paramType, context), paramType);
}
}
}
getInferredTypes(context);
return getInferredTypes(context);
}
function checkTypeArguments(signature: Signature, typeArgumentNodes: TypeNode[], typeArgumentTypes: Type[], reportErrors: boolean, headMessage?: DiagnosticMessage): boolean {
@@ -15527,7 +15558,7 @@ namespace ts {
else {
Debug.assert(resultOfFailedInference.failedTypeParameterIndex >= 0);
const failedTypeParameter = candidateForTypeArgumentError.typeParameters[resultOfFailedInference.failedTypeParameterIndex];
const inferenceCandidates = getInferenceCandidates(resultOfFailedInference, resultOfFailedInference.failedTypeParameterIndex);
const inferenceCandidates = resultOfFailedInference.inferences[resultOfFailedInference.failedTypeParameterIndex].candidates;
let diagnosticChainHead = chainDiagnosticMessages(/*details*/ undefined, // details will be provided by call to reportNoCommonSupertypeError
Diagnostics.The_type_argument_for_type_parameter_0_cannot_be_inferred_from_the_usage_Consider_specifying_the_type_arguments_explicitly,
@@ -15600,7 +15631,7 @@ namespace ts {
let candidate: Signature;
let typeArgumentsAreValid: boolean;
const inferenceContext = originalCandidate.typeParameters
? createInferenceContext(originalCandidate, /*inferUnionTypes*/ false, /*useAnyForNoInferences*/ isInJavaScriptFile(node))
? createInferenceContext(originalCandidate, /*flags*/ isInJavaScriptFile(node) ? InferenceFlags.AnyDefault : 0)
: undefined;
while (true) {
@@ -15612,8 +15643,7 @@ namespace ts {
typeArgumentsAreValid = checkTypeArguments(candidate, typeArguments, typeArgumentTypes, /*reportErrors*/ false);
}
else {
inferTypeArguments(node, candidate, args, excludeArgument, inferenceContext);
typeArgumentTypes = inferenceContext.inferredTypes;
typeArgumentTypes = inferTypeArguments(node, candidate, args, excludeArgument, inferenceContext);
typeArgumentsAreValid = inferenceContext.failedTypeParameterIndex === undefined;
}
if (!typeArgumentsAreValid) {
@@ -16187,7 +16217,7 @@ namespace ts {
for (let i = 0; i < len; i++) {
const declaration = <ParameterDeclaration>signature.parameters[i].valueDeclaration;
if (declaration.type) {
inferTypesWithContext(mapper.context, getTypeFromTypeNode(declaration.type), getTypeAtPosition(context, i));
inferTypes((<InferenceContext>mapper).inferences, getTypeFromTypeNode(declaration.type), getTypeAtPosition(context, i));
}
}
}
@@ -16273,7 +16303,7 @@ namespace ts {
// T in the second overload so that we do not infer Base as a candidate for T
// (inferring Base would make type argument inference inconsistent between the two
// overloads).
inferTypesWithContext(mapper.context, links.type, instantiateType(contextualType, mapper));
inferTypes((<InferenceContext>mapper).inferences, links.type, instantiateType(contextualType, mapper));
}
}

View File

@@ -3343,30 +3343,36 @@ namespace ts {
(t: TypeParameter): Type;
mappedTypes?: Type[]; // Types mapped by this mapper
instantiations?: Type[]; // Cache of instantiations created using this type mapper.
context?: InferenceContext; // The inference context this mapper was created from.
// Only inference mappers have this set (in createInferenceMapper).
// The identity mapper and regular instantiation mappers do not need it.
}
export const enum InferencePriority {
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
}
export interface InferenceInfo {
typeParameter: TypeParameter;
candidates: Type[];
inferredType: Type;
priority: InferencePriority;
topLevel: boolean;
isFixed: boolean;
}
export const enum InferenceFlags {
InferUnionTypes = 1 << 0, // Infer union types for disjoint candidates (otherwise unknownType)
NoDefault = 1 << 1, // Infer unknownType for no inferences (otherwise anyType or emptyObjectType)
AnyDefault = 1 << 2, // Infer anyType for no inferences (otherwise emptyObjectType)
}
/* @internal */
export interface TypeInferences {
primary: Type[]; // Inferences made directly to a type parameter
secondary: Type[]; // Inferences made to a type parameter in a union type
topLevel: boolean; // True if all inferences were made from top-level (not nested in object type) locations
isFixed: boolean; // Whether the type parameter is fixed, as defined in section 4.12.2 of the TypeScript spec
// If a type parameter is fixed, no more inferences can be made for the type parameter
}
/* @internal */
export interface InferenceContext {
export interface InferenceContext extends TypeMapper {
signature: Signature; // Generic signature for which inferences are made
inferUnionTypes: boolean; // Infer union types for disjoint candidates (otherwise undefinedType)
inferences: TypeInferences[]; // Inferences made for each type parameter
inferredTypes: Type[]; // Inferred type for each type parameter
mapper?: TypeMapper; // Type mapper for this inference context
inferences: InferenceInfo[]; // Inferences made for each type parameter
flags: InferenceFlags; // Inference flags
failedTypeParameterIndex?: number; // Index of type parameter for which inference failed
// It is optional because in contextual signature instantiation, nothing fails
useAnyForNoInferences?: boolean; // Use any instead of {} for no inferences
}
/* @internal */