Merge pull request #22197 from Microsoft/fixConditionalTypes

Conditional type fixes
This commit is contained in:
Anders Hejlsberg
2018-03-01 10:27:15 -08:00
committed by GitHub
15 changed files with 239 additions and 169 deletions

View File

@@ -318,7 +318,6 @@ namespace ts {
const intersectionTypes = createMap<IntersectionType>();
const literalTypes = createMap<LiteralType>();
const indexedAccessTypes = createMap<IndexedAccessType>();
const conditionalTypes = createMap<ConditionalType>();
const evolvingArrayTypes: EvolvingArrayType[] = [];
const undefinedProperties = createMap<Symbol>() as UnderscoreEscapedMap<Symbol>;
@@ -600,6 +599,7 @@ namespace ts {
ObjectType = 1 << 9,
EmptyObject = 1 << 10,
Union = 1 << 11,
Wildcard = 1 << 12,
}
const enum MembersOrExportsResolutionKind {
@@ -2975,8 +2975,8 @@ namespace ts {
if (type.flags & TypeFlags.Conditional) {
const checkTypeNode = typeToTypeNodeHelper((<ConditionalType>type).checkType, context);
const extendsTypeNode = typeToTypeNodeHelper((<ConditionalType>type).extendsType, context);
const trueTypeNode = typeToTypeNodeHelper((<ConditionalType>type).trueType, context);
const falseTypeNode = typeToTypeNodeHelper((<ConditionalType>type).falseType, context);
const trueTypeNode = typeToTypeNodeHelper(getTrueTypeFromConditionalType(<ConditionalType>type), context);
const falseTypeNode = typeToTypeNodeHelper(getFalseTypeFromConditionalType(<ConditionalType>type), context);
return createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode);
}
if (type.flags & TypeFlags.Substitution) {
@@ -5899,8 +5899,7 @@ namespace ts {
// Create a mapper from T to the current iteration type constituent. Then, if the
// mapped type is itself an instantiated type, combine the iteration mapper with the
// instantiation mapper.
const iterationMapper = createTypeMapper([typeParameter], [t]);
const templateMapper = type.mapper ? combineTypeMappers(type.mapper, iterationMapper) : iterationMapper;
const templateMapper = combineTypeMappers(type.mapper, createTypeMapper([typeParameter], [t]));
const propType = instantiateType(templateType, templateMapper);
// If the current iteration type constituent is a string literal type, create a property.
// Otherwise, for type string create a string index signature.
@@ -6122,22 +6121,20 @@ namespace ts {
}
function getDefaultConstraintOfConditionalType(type: ConditionalType) {
return getUnionType([type.trueType, type.falseType]);
return getUnionType([getTrueTypeFromConditionalType(type), getFalseTypeFromConditionalType(type)]);
}
function getConstraintOfDistributiveConditionalType(type: ConditionalType) {
function getConstraintOfDistributiveConditionalType(type: ConditionalType): Type {
// Check if we have a conditional type of the form 'T extends U ? X : Y', where T is a constrained
// type parameter. If so, create an instantiation of the conditional type where T is replaced
// with its constraint. We do this because if the constraint is a union type it will be distributed
// over the conditional type and possibly reduced. For example, 'T extends undefined ? never : T'
// removes 'undefined' from T.
if (isDistributiveConditionalType(type)) {
if (type.root.isDistributive) {
const constraint = getConstraintOfType(type.checkType);
if (constraint) {
const target = type.target || type;
const mapper = createTypeMapper([<TypeParameter>target.checkType], [constraint]);
const combinedMapper = type.mapper ? combineTypeMappers(mapper, type.mapper) : mapper;
return instantiateType(target, combinedMapper);
const mapper = createTypeMapper([<TypeParameter>type.root.checkType], [constraint]);
return getConditionalTypeInstantiation(type, combineTypeMappers(mapper, type.mapper));
}
}
return undefined;
@@ -6236,7 +6233,8 @@ namespace ts {
return baseIndexedAccess && baseIndexedAccess !== unknownType ? getBaseConstraint(baseIndexedAccess) : undefined;
}
if (t.flags & TypeFlags.Conditional) {
return getBaseConstraint(getConstraintOfConditionalType(<ConditionalType>t));
const constraint = getConstraintOfConditionalType(<ConditionalType>t);
return constraint && getBaseConstraint(constraint);
}
if (t.flags & TypeFlags.Substitution) {
return getBaseConstraint((<SubstitutionType>t).substitute);
@@ -7639,6 +7637,7 @@ namespace ts {
}
else if (flags & TypeFlags.Any) {
includes |= TypeIncludes.Any;
if (type === wildcardType) includes |= TypeIncludes.Wildcard;
}
else if (!strictNullChecks && flags & TypeFlags.Nullable) {
if (flags & TypeFlags.Undefined) includes |= TypeIncludes.Undefined;
@@ -7758,7 +7757,7 @@ namespace ts {
const typeSet: Type[] = [];
const includes = addTypesToUnion(typeSet, 0, types);
if (includes & TypeIncludes.Any) {
return anyType;
return includes & TypeIncludes.Wildcard ? wildcardType : anyType;
}
switch (unionReduction) {
case UnionReduction.Literal:
@@ -8140,7 +8139,7 @@ namespace ts {
function substituteIndexedMappedType(objectType: MappedType, type: IndexedAccessType) {
const mapper = createTypeMapper([getTypeParameterFromMappedType(objectType)], [type.indexType]);
const templateMapper = objectType.mapper ? combineTypeMappers(objectType.mapper, mapper) : mapper;
const templateMapper = combineTypeMappers(objectType.mapper, mapper);
return instantiateType(getTemplateTypeFromMappedType(objectType), templateMapper);
}
@@ -8207,76 +8206,59 @@ namespace ts {
return type.flags & TypeFlags.Substitution ? (<SubstitutionType>type).typeParameter : type;
}
function createConditionalType(checkType: Type, extendsType: Type, trueType: Type, falseType: Type, inferTypeParameters: TypeParameter[], target: ConditionalType, mapper: TypeMapper, aliasSymbol: Symbol, aliasTypeArguments: Type[]) {
const type = <ConditionalType>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;
type.aliasTypeArguments = aliasTypeArguments;
return 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);
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper): Type {
const checkType = instantiateType(root.checkType, mapper);
const extendsType = instantiateType(root.extendsType, mapper);
// Return falseType for a definitely false extends check. We check an instantations of the two
// types with type parameters mapped to the wildcard type, the most permissive instantiations
// possible (the wildcard type is assignable to and from all types). If those are not related,
// then no instatiations will be and we can just return the false branch type.
if (!typeMaybeAssignableTo(getWildcardInstantiation(checkType), getWildcardInstantiation(extendsType))) {
return instantiateType(baseFalseType, mapper);
return instantiateType(root.falseType, mapper);
}
// The check could be true for some instantiation
let combinedMapper: TypeMapper;
if (inferTypeParameters) {
const inferences = map(inferTypeParameters, createInferenceInfo);
if (root.inferTypeParameters) {
const inferences = map(root.inferTypeParameters, createInferenceInfo);
// We don't want inferences from constraints as they may cause us to eagerly resolve the
// conditional type instead of deferring resolution. Also, we always want strict function
// types rules (i.e. proper contravariance) for inferences.
inferTypes(inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
// 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;
// We infer {} when there are no candidates for a type parameter
const inferredTypes = map(inferences, inference => getTypeFromInference(inference) || emptyObjectType);
combinedMapper = combineTypeMappers(mapper, createTypeMapper(root.inferTypeParameters, inferredTypes));
}
// Return union of trueType and falseType for any and never since they match anything
if (checkType.flags & TypeFlags.Any || (checkType.flags & TypeFlags.Never && !(extendsType.flags & TypeFlags.Never))) {
return getUnionType([instantiateType(baseTrueType, combinedMapper || mapper), instantiateType(baseFalseType, mapper)]);
// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any) {
return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]);
}
// Instantiate the extends type including inferences for 'infer T' type parameters
const inferredExtendsType = combinedMapper ? instantiateType(baseExtendsType, combinedMapper) : extendsType;
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
// Return trueType for a definitely true extends check. The definitely assignable relation excludes
// type variable constraints from consideration. Without the definitely assignable relation, the type
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
// would immediately resolve to 'string' instead of being deferred.
if (checkTypeRelatedTo(checkType, inferredExtendsType, definitelyAssignableRelation, /*errorNode*/ undefined)) {
return instantiateType(baseTrueType, combinedMapper || mapper);
return instantiateType(root.trueType, combinedMapper || mapper);
}
// Return a deferred type for a check that is neither definitely true nor definitely false
const erasedCheckType = getActualTypeParameter(checkType);
const trueType = instantiateType(baseTrueType, mapper);
const falseType = instantiateType(baseFalseType, mapper);
// We compute the cache key from the ids of the four constituent types, plus an indicator of whether the
// type is distributive (i.e. whether the original declaration has a type parameter as the check type).
const isDistributive = (target ? target.checkType : erasedCheckType).flags & TypeFlags.TypeParameter ? 1 : 0;
const id = erasedCheckType.id + "," + extendsType.id + "," + trueType.id + "," + falseType.id + "," + isDistributive;
const cached = conditionalTypes.get(id);
if (cached) {
return cached;
}
const result = createConditionalType(erasedCheckType, extendsType, trueType, falseType,
inferTypeParameters, target, mapper, aliasSymbol, instantiateTypes(baseAliasTypeArguments, mapper));
conditionalTypes.set(id, result);
const result = <ConditionalType>createType(TypeFlags.Conditional);
result.root = root;
result.checkType = erasedCheckType;
result.extendsType = extendsType;
result.mapper = mapper;
result.aliasSymbol = root.aliasSymbol;
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper);
return result;
}
function isDistributiveConditionalType(type: ConditionalType) {
return !!((type.target || type).checkType.flags & TypeFlags.TypeParameter);
function getTrueTypeFromConditionalType(type: ConditionalType) {
return type.resolvedTrueType || (type.resolvedTrueType = instantiateType(type.root.trueType, type.mapper));
}
function getFalseTypeFromConditionalType(type: ConditionalType) {
return type.resolvedFalseType || (type.resolvedFalseType = instantiateType(type.root.falseType, type.mapper));
}
function getInferTypeParameters(node: ConditionalTypeNode): TypeParameter[] {
@@ -8294,11 +8276,28 @@ namespace ts {
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),
getInferTypeParameters(node), /*target*/ undefined, /*mapper*/ undefined,
getAliasSymbolForTypeNode(node), getAliasTypeArgumentsForTypeNode(node));
const checkType = getTypeFromTypeNode(node.checkType);
const aliasTypeArguments = getAliasTypeArgumentsForTypeNode(node);
const allOuterTypeParameters = getOuterTypeParameters(node, /*includeThisTypes*/ true);
const outerTypeParameters = aliasTypeArguments ? allOuterTypeParameters : filter(allOuterTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, node));
const root: ConditionalRoot = {
node,
checkType,
extendsType: getTypeFromTypeNode(node.extendsType),
trueType: getTypeFromTypeNode(node.trueType),
falseType: getTypeFromTypeNode(node.falseType),
isDistributive: !!(checkType.flags & TypeFlags.TypeParameter),
inferTypeParameters: getInferTypeParameters(node),
outerTypeParameters,
instantiations: undefined,
aliasSymbol: getAliasSymbolForTypeNode(node),
aliasTypeArguments
};
links.resolvedType = getConditionalType(root, /*mapper*/ undefined);
if (outerTypeParameters) {
root.instantiations = createMap<Type>();
root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType);
}
}
return links.resolvedType;
}
@@ -8689,6 +8688,8 @@ namespace ts {
}
function combineTypeMappers(mapper1: TypeMapper, mapper2: TypeMapper): TypeMapper {
if (!mapper1) return mapper2;
if (!mapper2) return mapper1;
return t => instantiateType(mapper1(t), mapper2);
}
@@ -8886,24 +8887,36 @@ namespace ts {
}
function getConditionalTypeInstantiation(type: ConditionalType, mapper: TypeMapper): Type {
const target = type.target || type;
const combinedMapper = type.mapper ? combineTypeMappers(type.mapper, mapper) : mapper;
const root = type.root;
if (root.outerTypeParameters) {
// We are instantiating a conditional type that has one or more type parameters in scope. Apply the
// mapper to the type parameters to produce the effective list of type arguments, and compute the
// instantiation cache key from the type IDs of the type arguments.
const typeArguments = map(root.outerTypeParameters, mapper);
const id = getTypeListId(typeArguments);
let result = root.instantiations.get(id);
if (!result) {
const newMapper = createTypeMapper(root.outerTypeParameters, typeArguments);
result = instantiateConditionalType(root, newMapper);
root.instantiations.set(id, result);
}
return result;
}
return type;
}
function instantiateConditionalType(root: ConditionalRoot, mapper: TypeMapper): Type {
// Check if we have a conditional type where the check type is a naked type parameter. If so,
// the conditional type is distributive over union types and when T is instantiated to a union
// type A | B, we produce (A extends U ? X : Y) | (B extends U ? X : Y).
if (isDistributiveConditionalType(target)) {
const checkType = <TypeParameter>target.checkType;
const instantiatedType = combinedMapper(checkType);
if (checkType !== instantiatedType && instantiatedType.flags & TypeFlags.Union) {
return mapType(instantiatedType, t => instantiateConditionalType(target, createReplacementMapper(checkType, t, combinedMapper)));
if (root.isDistributive) {
const checkType = <TypeParameter>root.checkType;
const instantiatedType = mapper(checkType);
if (checkType !== instantiatedType && instantiatedType.flags & (TypeFlags.Union | TypeFlags.Never)) {
return mapType(instantiatedType, t => getConditionalType(root, createReplacementMapper(checkType, t, mapper)));
}
}
return instantiateConditionalType(target, combinedMapper);
}
function instantiateConditionalType(type: ConditionalType, mapper: TypeMapper): Type {
return getConditionalType(instantiateType(type.checkType, mapper), type.extendsType, type.trueType, type.falseType,
type.inferTypeParameters, type, mapper, type.aliasSymbol, type.aliasTypeArguments);
return getConditionalType(root, mapper);
}
function instantiateType(type: Type, mapper: TypeMapper): Type {
@@ -8945,7 +8958,7 @@ namespace ts {
return getIndexedAccessType(instantiateType((<IndexedAccessType>type).objectType, mapper), instantiateType((<IndexedAccessType>type).indexType, mapper));
}
if (type.flags & TypeFlags.Conditional) {
return getConditionalTypeInstantiation(<ConditionalType>type, mapper);
return getConditionalTypeInstantiation(<ConditionalType>type, combineTypeMappers((<ConditionalType>type).mapper, mapper));
}
if (type.flags & TypeFlags.Substitution) {
return mapper((<SubstitutionType>type).typeParameter);
@@ -9691,11 +9704,11 @@ namespace ts {
}
}
if (flags & TypeFlags.Conditional) {
if (result = isRelatedTo((<ConditionalType>source).checkType, (<ConditionalType>target).checkType, /*reportErrors*/ false)) {
if (result &= isRelatedTo((<ConditionalType>source).extendsType, (<ConditionalType>target).extendsType, /*reportErrors*/ false)) {
if (result &= isRelatedTo((<ConditionalType>source).trueType, (<ConditionalType>target).trueType, /*reportErrors*/ false)) {
if (result &= isRelatedTo((<ConditionalType>source).falseType, (<ConditionalType>target).falseType, /*reportErrors*/ false)) {
if (isDistributiveConditionalType(<ConditionalType>source) === isDistributiveConditionalType(<ConditionalType>target)) {
if ((<ConditionalType>source).root.isDistributive === (<ConditionalType>target).root.isDistributive) {
if (result = isRelatedTo((<ConditionalType>source).checkType, (<ConditionalType>target).checkType, /*reportErrors*/ false)) {
if (result &= isRelatedTo((<ConditionalType>source).extendsType, (<ConditionalType>target).extendsType, /*reportErrors*/ false)) {
if (result &= isRelatedTo(getTrueTypeFromConditionalType(<ConditionalType>source), getTrueTypeFromConditionalType(<ConditionalType>target), /*reportErrors*/ false)) {
if (result &= isRelatedTo(getFalseTypeFromConditionalType(<ConditionalType>source), getFalseTypeFromConditionalType(<ConditionalType>target), /*reportErrors*/ false)) {
return result;
}
}
@@ -10085,20 +10098,11 @@ namespace ts {
}
}
else if (source.flags & TypeFlags.Conditional) {
if (relation !== definitelyAssignableRelation) {
const constraint = getConstraintOfDistributiveConditionalType(<ConditionalType>source);
if (constraint) {
if (result = isRelatedTo(constraint, target, reportErrors)) {
errorInfo = saveErrorInfo;
return result;
}
}
}
if (target.flags & TypeFlags.Conditional) {
if (isTypeIdenticalTo((<ConditionalType>source).checkType, (<ConditionalType>target).checkType) &&
isTypeIdenticalTo((<ConditionalType>source).extendsType, (<ConditionalType>target).extendsType)) {
if (result = isRelatedTo((<ConditionalType>source).trueType, (<ConditionalType>target).trueType, reportErrors)) {
result &= isRelatedTo((<ConditionalType>source).falseType, (<ConditionalType>target).falseType, reportErrors);
if (result = isRelatedTo(getTrueTypeFromConditionalType(<ConditionalType>source), getTrueTypeFromConditionalType(<ConditionalType>target), reportErrors)) {
result &= isRelatedTo(getFalseTypeFromConditionalType(<ConditionalType>source), getFalseTypeFromConditionalType(<ConditionalType>target), reportErrors);
}
if (result) {
errorInfo = saveErrorInfo;
@@ -10106,9 +10110,21 @@ namespace ts {
}
}
}
else if (result = isRelatedTo(getDefaultConstraintOfConditionalType(<ConditionalType>source), target, reportErrors)) {
errorInfo = saveErrorInfo;
return result;
else if (relation !== definitelyAssignableRelation) {
const distributiveConstraint = getConstraintOfDistributiveConditionalType(<ConditionalType>source);
if (distributiveConstraint) {
if (result = isRelatedTo(distributiveConstraint, target, reportErrors)) {
errorInfo = saveErrorInfo;
return result;
}
}
const defaultConstraint = getDefaultConstraintOfConditionalType(<ConditionalType>source);
if (defaultConstraint) {
if (result = isRelatedTo(defaultConstraint, target, reportErrors)) {
errorInfo = saveErrorInfo;
return result;
}
}
}
}
else {
@@ -11479,12 +11495,23 @@ namespace ts {
let symbolStack: Symbol[];
let visited: Map<boolean>;
let contravariant = false;
let propagationType: Type;
inferFromTypes(originalSource, originalTarget);
function inferFromTypes(source: Type, target: Type) {
if (!couldContainTypeVariables(target)) {
return;
}
if (source.flags & (TypeFlags.Any | TypeFlags.Never) && source !== silentNeverType) {
// We are inferring from 'any' or 'never'. We want to infer this type for every type parameter
// referenced in the target type, so we record the propagation type and infer from the target
// to itself. Then, as we find candidates we substitute the propagation type.
const savePropagationType = propagationType;
propagationType = source;
inferFromTypes(target, target);
propagationType = savePropagationType;
return;
}
if (source.aliasSymbol && source.aliasTypeArguments && source.aliasSymbol === target.aliasSymbol) {
// Source and target are types originating in the same generic type alias declaration.
// Simply infer from source type arguments to target type arguments.
@@ -11552,11 +11579,12 @@ namespace ts {
inference.priority = priority;
}
if (priority === inference.priority) {
const candidate = propagationType || source;
if (contravariant) {
inference.contraCandidates = append(inference.contraCandidates, source);
inference.contraCandidates = append(inference.contraCandidates, candidate);
}
else {
inference.candidates = append(inference.candidates, source);
inference.candidates = append(inference.candidates, candidate);
}
}
if (!(priority & InferencePriority.ReturnType) && target.flags & TypeFlags.TypeParameter && !isTypeParameterAtTopLevel(originalTarget, <TypeParameter>target)) {
@@ -11599,8 +11627,8 @@ namespace ts {
else if (source.flags & TypeFlags.Conditional && target.flags & TypeFlags.Conditional) {
inferFromTypes((<ConditionalType>source).checkType, (<ConditionalType>target).checkType);
inferFromTypes((<ConditionalType>source).extendsType, (<ConditionalType>target).extendsType);
inferFromTypes((<ConditionalType>source).trueType, (<ConditionalType>target).trueType);
inferFromTypes((<ConditionalType>source).falseType, (<ConditionalType>target).falseType);
inferFromTypes(getTrueTypeFromConditionalType(<ConditionalType>source), getTrueTypeFromConditionalType(<ConditionalType>target));
inferFromTypes(getFalseTypeFromConditionalType(<ConditionalType>source), getFalseTypeFromConditionalType(<ConditionalType>target));
}
else if (target.flags & TypeFlags.UnionOrIntersection) {
const targetTypes = (<UnionOrIntersectionType>target).types;
@@ -12424,6 +12452,9 @@ namespace ts {
// is a union type, the mapping function is applied to each constituent type and a union
// of the resulting types is returned.
function mapType(type: Type, mapper: (t: Type) => Type, noReductions?: boolean): Type {
if (type.flags & TypeFlags.Never) {
return type;
}
if (!(type.flags & TypeFlags.Union)) {
return mapper(type);
}

View File

@@ -3821,16 +3821,27 @@ namespace ts {
type: InstantiableType | UnionOrIntersectionType;
}
// T extends U ? X : Y (TypeFlags.Conditional)
export interface ConditionalType extends InstantiableType {
export interface ConditionalRoot {
node: ConditionalTypeNode;
checkType: Type;
extendsType: Type;
trueType: Type;
falseType: Type;
/* @internal */
isDistributive: boolean;
inferTypeParameters: TypeParameter[];
/* @internal */
target?: ConditionalType;
outerTypeParameters?: TypeParameter[];
instantiations?: Map<Type>;
aliasSymbol: Symbol;
aliasTypeArguments: Type[];
}
// T extends U ? X : Y (TypeFlags.Conditional)
export interface ConditionalType extends InstantiableType {
root: ConditionalRoot;
checkType: Type;
extendsType: Type;
resolvedTrueType?: Type;
resolvedFalseType?: Type;
/* @internal */
mapper?: TypeMapper;
}