Reduce intersections by discriminants (#36696)

* Treat never-like intersections as never

* Accept new baselines

* Fix compiler issues revealed by increased intersection correctness

* Delete fourslash tests that are no longer applicable

* Include isNeverLikeIntersection check in getNormalizedType

* Erase never-like types in several more places

* Check that base types are not never-like

* Add comments

* Revert isNeverLikeType check in getIndexType (keyof shouldn't resolve member types)

* Introduce getReducedType for union and intersection types

* Don't reduce in getApparentType

* Avoid relationship check in resolveMappedTypeMembers

* Accept new baselines

* Don't call getReducedType in getIndexType

* Ensure reduced and unreduced forms of a type can compare identical

* Reduce types before converting them to string representation

* Accept new baselines

* Reduce intersections before obtaining keyof X

* Add tests

* Accept new baselines

* Fix comment in tests

* Don't infer from empty intersection types

* Add tests

* Accept new baselines

* Defer instantiation of mapped type property types

* Accept new baselines

* Include more precise type in diagnostic

* Accept new baselines

* Minor optimization

* Improve error message

* Optional properties in intersections are never discriminants
This commit is contained in:
Anders Hejlsberg
2020-02-28 17:06:45 -08:00
committed by GitHub
parent f31ff2dac0
commit be4b814a4c
32 changed files with 1232 additions and 190 deletions

View File

@@ -4077,6 +4077,8 @@ namespace ts {
return undefined!; // TODO: GH#18217
}
type = getReducedType(type);
if (type.flags & TypeFlags.Any) {
context.approximateLength += 3;
return createKeywordTypeNode(SyntaxKind.AnyKeyword);
@@ -7145,6 +7147,7 @@ namespace ts {
let type: Type | undefined;
if (pattern.kind === SyntaxKind.ObjectBindingPattern) {
if (declaration.dotDotDotToken) {
parentType = getReducedType(parentType);
if (parentType.flags & TypeFlags.Unknown || !isValidSpreadType(parentType)) {
error(declaration, Diagnostics.Rest_types_may_only_be_created_from_object_types);
return errorType;
@@ -8022,13 +8025,17 @@ namespace ts {
}
function getTypeOfSymbol(symbol: Symbol): Type {
if (getCheckFlags(symbol) & CheckFlags.DeferredType) {
const checkFlags = getCheckFlags(symbol);
if (checkFlags & CheckFlags.DeferredType) {
return getTypeOfSymbolWithDeferredType(symbol);
}
if (getCheckFlags(symbol) & CheckFlags.Instantiated) {
if (checkFlags & CheckFlags.Instantiated) {
return getTypeOfInstantiatedSymbol(symbol);
}
if (getCheckFlags(symbol) & CheckFlags.ReverseMapped) {
if (checkFlags & CheckFlags.Mapped) {
return getTypeOfMappedSymbol(symbol as MappedSymbol);
}
if (checkFlags & CheckFlags.ReverseMapped) {
return getTypeOfReverseMappedSymbol(symbol as ReverseMappedSymbol);
}
if (symbol.flags & (SymbolFlags.Variable | SymbolFlags.Property)) {
@@ -8329,7 +8336,7 @@ namespace ts {
error(baseTypeNode.expression, Diagnostics.No_base_constructor_has_the_specified_number_of_type_arguments);
return type.resolvedBaseTypes = emptyArray;
}
baseType = getReturnTypeOfSignature(constructors[0]);
baseType = getReducedType(getReturnTypeOfSignature(constructors[0]));
}
if (baseType === errorType) {
@@ -8376,8 +8383,8 @@ namespace ts {
}
// TODO: Given that we allow type parmeters here now, is this `!isGenericMappedType(type)` check really needed?
// There's no reason a `T` should be allowed while a `Readonly<T>` should not.
return !!(type.flags & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.Any)) && !isGenericMappedType(type) ||
!!(type.flags & TypeFlags.Intersection) && every((<IntersectionType>type).types, isValidBaseType);
return !!(type.flags & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.Any) && !isGenericMappedType(type) ||
type.flags & TypeFlags.Intersection && every((<IntersectionType>type).types, isValidBaseType));
}
function resolveBaseTypesOfInterface(type: InterfaceType): void {
@@ -8385,7 +8392,7 @@ namespace ts {
for (const declaration of type.symbol.declarations) {
if (declaration.kind === SyntaxKind.InterfaceDeclaration && getInterfaceBaseTypeNodes(<InterfaceDeclaration>declaration)) {
for (const node of getInterfaceBaseTypeNodes(<InterfaceDeclaration>declaration)!) {
const baseType = getTypeFromTypeNode(node);
const baseType = getReducedType(getTypeFromTypeNode(node));
if (baseType !== errorType) {
if (isValidBaseType(baseType)) {
if (type !== baseType && !hasBaseType(baseType, type)) {
@@ -9619,7 +9626,6 @@ namespace ts {
// mapped type is itself an instantiated type, combine the iteration mapper with the
// instantiation mapper.
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.
if (isTypeUsableAsPropertyName(t)) {
@@ -9629,13 +9635,11 @@ namespace ts {
!(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly ||
!(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp));
const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, isReadonly ? CheckFlags.Readonly : 0);
// When creating an optional property in strictNullChecks mode, if 'undefined' isn't assignable to the
// type, we include 'undefined' in the type. Similarly, when creating a non-optional property in strictNullChecks
// mode, if the underlying property is optional we remove 'undefined' from the type.
prop.type = strictNullChecks && isOptional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType) :
strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) :
propType;
const stripOptional = strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional;
const prop = <MappedSymbol>createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName,
CheckFlags.Mapped | (isReadonly ? CheckFlags.Readonly : 0) | (stripOptional ? CheckFlags.StripOptional : 0));
prop.mappedType = type;
prop.mapper = templateMapper;
if (modifiersProp) {
prop.syntheticOrigin = modifiersProp;
prop.declarations = modifiersProp.declarations;
@@ -9643,16 +9647,41 @@ namespace ts {
prop.nameType = t;
members.set(propName, prop);
}
else if (t.flags & (TypeFlags.Any | TypeFlags.String)) {
stringIndexInfo = createIndexInfo(propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly));
}
else if (t.flags & (TypeFlags.Number | TypeFlags.Enum)) {
numberIndexInfo = createIndexInfo(numberIndexInfo ? getUnionType([numberIndexInfo.type, propType]) : propType,
!!(templateModifiers & MappedTypeModifiers.IncludeReadonly));
else if (t.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.Enum)) {
const propType = instantiateType(templateType, templateMapper);
if (t.flags & (TypeFlags.Any | TypeFlags.String)) {
stringIndexInfo = createIndexInfo(propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly));
}
else {
numberIndexInfo = createIndexInfo(numberIndexInfo ? getUnionType([numberIndexInfo.type, propType]) : propType,
!!(templateModifiers & MappedTypeModifiers.IncludeReadonly));
}
}
}
}
function getTypeOfMappedSymbol(symbol: MappedSymbol) {
if (!symbol.type) {
if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) {
return errorType;
}
const templateType = getTemplateTypeFromMappedType(<MappedType>symbol.mappedType.target || symbol.mappedType);
const propType = instantiateType(templateType, symbol.mapper);
// When creating an optional property in strictNullChecks mode, if 'undefined' isn't assignable to the
// type, we include 'undefined' in the type. Similarly, when creating a non-optional property in strictNullChecks
// mode, if the underlying property is optional we remove 'undefined' from the type.
let type = strictNullChecks && symbol.flags & SymbolFlags.Optional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType) :
symbol.checkFlags & CheckFlags.StripOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) :
propType;
if (!popTypeResolution()) {
error(currentNode, Diagnostics.Type_of_property_0_circularly_references_itself_in_mapped_type_1, symbolToString(symbol), typeToString(symbol.mappedType));
type = errorType;
}
symbol.type = type;
}
return symbol.type;
}
function getTypeParameterFromMappedType(type: MappedType) {
return type.typeParameter ||
(type.typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(type.declaration.typeParameter)));
@@ -9800,7 +9829,7 @@ namespace ts {
}
function getPropertiesOfType(type: Type): Symbol[] {
type = getApparentType(type);
type = getApparentType(getReducedType(type));
return type.flags & TypeFlags.UnionOrIntersection ?
getPropertiesOfUnionOrIntersectionType(<UnionType>type) :
getPropertiesOfObjectType(type);
@@ -10141,7 +10170,7 @@ namespace ts {
/**
* For a type parameter, return the base constraint of the type parameter. For the string, number,
* boolean, and symbol primitive types, return the corresponding object types. Otherwise return the
* type itself. Note that the apparent type of a union type is the union type itself.
* type itself.
*/
function getApparentType(type: Type): Type {
const t = type.flags & TypeFlags.Instantiable ? getBaseConstraintOfType(type) || unknownType : type;
@@ -10169,7 +10198,7 @@ namespace ts {
let checkFlags = 0;
for (const current of containingType.types) {
const type = getApparentType(current);
if (type !== errorType) {
if (!(type === errorType || type.flags & TypeFlags.Never)) {
const prop = getPropertyOfType(type, name);
const modifiers = prop ? getDeclarationModifierFlagsFromSymbol(prop) : 0;
if (prop && !(modifiers & excludeModifiers)) {
@@ -10240,6 +10269,9 @@ namespace ts {
if (isLiteralType(type)) {
checkFlags |= CheckFlags.HasLiteralType;
}
if (type.flags & TypeFlags.Never) {
checkFlags |= CheckFlags.HasNeverType;
}
propTypes.push(type);
}
addRange(propTypes, indexTypes);
@@ -10291,6 +10323,44 @@ namespace ts {
return property && !(getCheckFlags(property) & CheckFlags.ReadPartial) ? property : undefined;
}
/**
* Return the reduced form of the given type. For a union type, it is a union of the normalized constituent types.
* For an intersection of types containing one or more mututally exclusive discriminant properties, it is 'never'.
* For all other types, it is simply the type itself. Discriminant properties are considered mutually exclusive when
* no constituent property has type 'never', but the intersection of the constituent property types is 'never'.
*/
function getReducedType(type: Type): Type {
if (type.flags & TypeFlags.Union && (<UnionType>type).objectFlags & ObjectFlags.ContainsIntersections) {
return (<UnionType>type).resolvedReducedType || ((<UnionType>type).resolvedReducedType = getReducedUnionType(<UnionType>type));
}
else if (type.flags & TypeFlags.Intersection) {
if (!((<IntersectionType>type).objectFlags & ObjectFlags.IsNeverIntersectionComputed)) {
(<IntersectionType>type).objectFlags |= ObjectFlags.IsNeverIntersectionComputed |
(some(getPropertiesOfUnionOrIntersectionType(<IntersectionType>type), isDiscriminantWithNeverType) ? ObjectFlags.IsNeverIntersection : 0);
}
return (<IntersectionType>type).objectFlags & ObjectFlags.IsNeverIntersection ? neverType : type;
}
return type;
}
function getReducedUnionType(unionType: UnionType) {
const reducedTypes = sameMap(unionType.types, getReducedType);
if (reducedTypes === unionType.types) {
return unionType;
}
const reduced = getUnionType(reducedTypes);
if (reduced.flags & TypeFlags.Union) {
(<UnionType>reduced).resolvedReducedType = reduced;
}
return reduced;
}
function isDiscriminantWithNeverType(prop: Symbol) {
return !(prop.flags & SymbolFlags.Optional) &&
(getCheckFlags(prop) & (CheckFlags.Discriminant | CheckFlags.HasNeverType)) === CheckFlags.Discriminant &&
!!(getTypeOfSymbol(prop).flags & TypeFlags.Never);
}
/**
* Return the symbol for the property with the given name in the given type. Creates synthetic union properties when
* necessary, maps primitive types and type parameters are to their apparent types, and augments with properties from
@@ -10300,7 +10370,7 @@ namespace ts {
* @param name a name of property to look up in a given type
*/
function getPropertyOfType(type: Type, name: __String): Symbol | undefined {
type = getApparentType(type);
type = getApparentType(getReducedType(type));
if (type.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(<ObjectType>type);
const symbol = resolved.members.get(name);
@@ -10338,7 +10408,7 @@ namespace ts {
* maps primitive types and type parameters are to their apparent types.
*/
function getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[] {
return getSignaturesOfStructuredType(getApparentType(type), kind);
return getSignaturesOfStructuredType(getApparentType(getReducedType(type)), kind);
}
function getIndexInfoOfStructuredType(type: Type, kind: IndexKind): IndexInfo | undefined {
@@ -10356,13 +10426,13 @@ namespace ts {
// Return the indexing info of the given kind in the given type. Creates synthetic union index types when necessary and
// maps primitive types and type parameters are to their apparent types.
function getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined {
return getIndexInfoOfStructuredType(getApparentType(type), kind);
return getIndexInfoOfStructuredType(getApparentType(getReducedType(type)), kind);
}
// Return the index type of the given kind in the given type. Creates synthetic union index types when necessary and
// maps primitive types and type parameters are to their apparent types.
function getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined {
return getIndexTypeOfStructuredType(getApparentType(type), kind);
return getIndexTypeOfStructuredType(getApparentType(getReducedType(type)), kind);
}
function getImplicitIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined {
@@ -11932,7 +12002,9 @@ namespace ts {
neverType;
}
}
return getUnionTypeFromSortedList(typeSet, includes & TypeFlags.NotPrimitiveUnion ? 0 : ObjectFlags.PrimitiveUnion, aliasSymbol, aliasTypeArguments);
const objectFlags = (includes & TypeFlags.NotPrimitiveUnion ? 0 : ObjectFlags.PrimitiveUnion) |
(includes & TypeFlags.Intersection ? ObjectFlags.ContainsIntersections : 0);
return getUnionTypeFromSortedList(typeSet, objectFlags, aliasSymbol, aliasTypeArguments);
}
function getUnionTypePredicate(signatures: readonly Signature[]): TypePredicate | undefined {
@@ -12300,6 +12372,7 @@ namespace ts {
}
function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type {
type = getReducedType(type);
return type.flags & TypeFlags.Union ? getIntersectionType(map((<IntersectionType>type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.Intersection ? getUnionType(map((<IntersectionType>type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
@@ -12716,7 +12789,7 @@ namespace ts {
// In the following we resolve T[K] to the type of the property in T selected by K.
// We treat boolean as different from other unions to improve errors;
// skipping straight to getPropertyTypeForIndexType gives errors with 'boolean' instead of 'true'.
const apparentObjectType = getApparentType(objectType);
const apparentObjectType = getApparentType(getReducedType(objectType));
if (indexType.flags & TypeFlags.Union && !(indexType.flags & TypeFlags.Boolean)) {
const propTypes: Type[] = [];
let wasMissingProp = false;
@@ -13697,7 +13770,7 @@ namespace ts {
if (typeVariable) {
const mappedTypeVariable = instantiateType(typeVariable, mapper);
if (typeVariable !== mappedTypeVariable) {
return mapType(mappedTypeVariable, t => {
return mapType(getReducedType(mappedTypeVariable), t => {
if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && t !== errorType) {
const replacementMapper = createReplacementMapper(typeVariable, t, mapper);
return isArrayType(t) ? instantiateMappedArrayType(t, type, replacementMapper) :
@@ -14849,7 +14922,8 @@ namespace ts {
}
}
else {
if (!(source.flags === target.flags && source.flags & TypeFlags.Substructure)) return false;
if (!(source.flags & TypeFlags.UnionOrIntersection) && !(target.flags & TypeFlags.UnionOrIntersection) &&
source.flags !== target.flags && !(source.flags & TypeFlags.Substructure)) return false;
}
if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) {
const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation));
@@ -14868,15 +14942,16 @@ namespace ts {
}
function getNormalizedType(type: Type, writing: boolean): Type {
do {
while (true) {
const t = isFreshLiteralType(type) ? (<FreshableType>type).regularType :
getObjectFlags(type) & ObjectFlags.Reference && (<TypeReference>type).node ? createTypeReference((<TypeReference>type).target, getTypeArguments(<TypeReference>type)) :
type.flags & TypeFlags.UnionOrIntersection ? getReducedType(type) :
type.flags & TypeFlags.Substitution ? writing ? (<SubstitutionType>type).typeVariable : (<SubstitutionType>type).substitute :
type.flags & TypeFlags.Simplifiable ? getSimplifiedType(type, writing) :
type;
if (t === type) break;
type = t;
} while (true);
}
return type;
}
@@ -18164,6 +18239,7 @@ namespace ts {
}
}
else {
source = getReducedType(source);
if (!(priority & InferencePriority.NoConstraints && source.flags & (TypeFlags.Intersection | TypeFlags.Instantiable))) {
const apparentSource = getApparentType(source);
// getApparentType can return _any_ type, since an indexed access or conditional may simplify to any other type.
@@ -22584,7 +22660,7 @@ namespace ts {
hasComputedStringProperty = false;
hasComputedNumberProperty = false;
}
const type = checkExpression(memberDecl.expression);
const type = getReducedType(checkExpression(memberDecl.expression));
if (!isValidSpreadType(type)) {
error(memberDecl, Diagnostics.Spread_types_may_only_be_created_from_object_types);
return errorType;
@@ -22792,7 +22868,7 @@ namespace ts {
spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false);
attributesTable = createSymbolTable();
}
const exprType = checkExpressionCached(attributeDecl.expression, checkMode);
const exprType = getReducedType(checkExpressionCached(attributeDecl.expression, checkMode));
if (isTypeAny(exprType)) {
hasSpreadAnyType = true;
}
@@ -32676,7 +32752,7 @@ namespace ts {
}
checkTypeReferenceNode(typeRefNode);
if (produceDiagnostics) {
const t = getTypeFromTypeNode(typeRefNode);
const t = getReducedType(getTypeFromTypeNode(typeRefNode));
if (t !== errorType) {
if (isValidBaseType(t)) {
const genericDiag = t.symbol && t.symbol.flags & SymbolFlags.Class ?

View File

@@ -2365,6 +2365,10 @@
"category": "Error",
"code": 2614
},
"Type of property '{0}' circularly references itself in mapped type '{1}'.": {
"category": "Error",
"code": 2615
},
"Cannot augment module '{0}' with value exports because it resolves to a non-module entity.": {
"category": "Error",

View File

@@ -1795,9 +1795,7 @@ namespace ts {
const target = getTargetOfBindingOrAssignmentElement(bindingElement);
if (target && isPropertyName(target)) {
return isComputedPropertyName(target) && isStringOrNumericLiteral(target.expression)
? target.expression
: target;
return target;
}
}

View File

@@ -976,7 +976,7 @@ namespace ts {
);
}
function visitArrayAssignmentTarget(node: AssignmentPattern) {
function visitArrayAssignmentTarget(node: BindingOrAssignmentElement) {
const target = getTargetOfBindingOrAssignmentElement(node);
if (target && isPrivateIdentifierPropertyAccessExpression(target)) {
const wrapped = wrapPrivateIdentifierForDestructuringTarget(target);

View File

@@ -4155,6 +4155,9 @@ namespace ts {
OptionalParameter = 1 << 14, // Optional parameter
RestParameter = 1 << 15, // Rest parameter
DeferredType = 1 << 16, // Calculation of the type of this symbol is deferred due to processing costs, should be fetched with `getTypeOfSymbolWithDeferredType`
HasNeverType = 1 << 17, // Synthetic property with at least one never type in constituents
Mapped = 1 << 18, // Property of mapped type
StripOptional = 1 << 19, // Strip optionality in mapped property
Synthetic = SyntheticProperty | SyntheticMethod,
Discriminant = HasNonUniformType | HasLiteralType,
Partial = ReadPartial | WritePartial
@@ -4165,6 +4168,12 @@ namespace ts {
checkFlags: CheckFlags;
}
/* @internal */
export interface MappedSymbol extends TransientSymbol {
mappedType: MappedType;
mapper: TypeMapper;
}
/* @internal */
export interface ReverseMappedSymbol extends TransientSymbol {
propertyType: Type;
@@ -4362,16 +4371,16 @@ namespace ts {
NotPrimitiveUnion = Any | Unknown | Enum | Void | Never | StructuredOrInstantiable,
// The following flags are aggregated during union and intersection type construction
/* @internal */
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | NonPrimitive,
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive,
// The following flags are used for different purposes during union and intersection type construction
/* @internal */
IncludesStructuredOrInstantiable = TypeParameter,
/* @internal */
IncludesNonWideningType = Intersection,
IncludesNonWideningType = Index,
/* @internal */
IncludesWildcard = Index,
IncludesWildcard = IndexedAccess,
/* @internal */
IncludesEmptyObject = IndexedAccess,
IncludesEmptyObject = Conditional,
}
export type DestructuringPattern = BindingPattern | ObjectLiteralExpression | ArrayLiteralExpression;
@@ -4487,6 +4496,12 @@ namespace ts {
CouldContainTypeVariablesComputed = 1 << 26, // CouldContainTypeVariables flag has been computed
/* @internal */
CouldContainTypeVariables = 1 << 27, // Type could contain a type variable
/* @internal */
ContainsIntersections = 1 << 28, // Union contains intersections
/* @internal */
IsNeverIntersectionComputed = 1 << 28, // IsNeverLike flag has been computed
/* @internal */
IsNeverIntersection = 1 << 29, // Intersection reduces to never
ClassOrInterface = Class | Interface,
/* @internal */
RequiresWidening = ContainsWideningType | ContainsObjectOrArrayLiteral,
@@ -4608,6 +4623,8 @@ namespace ts {
}
export interface UnionType extends UnionOrIntersectionType {
/* @internal */
resolvedReducedType: Type;
}
export interface IntersectionType extends UnionOrIntersectionType {