From 2c8b1fab76141f9c7e0e8fbe841de2ccfc70b78f Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 12 Jul 2020 21:20:07 -0700 Subject: [PATCH] Reintroduce definitelyAssignableRelation and use with conditional types --- src/compiler/checker.ts | 97 +++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 58 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 082c18f3485..89ec2cd8a2a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -741,7 +741,6 @@ namespace ts { const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType; const numberOrBigIntType = getUnionType([numberType, bigintType]); - const restrictiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter(t) : t); const permissiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? wildcardType : t); const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); @@ -940,6 +939,7 @@ namespace ts { const subtypeRelation = createMap(); const strictSubtypeRelation = createMap(); const assignableRelation = createMap(); + const definitelyAssignableRelation = createMap(); const comparableRelation = createMap(); const identityRelation = createMap(); const enumRelation = createMap(); @@ -13576,7 +13576,7 @@ namespace ts { const falseType = getFalseTypeFromConditionalType(type); // Simplifications for types of the form `T extends U ? T : never` and `T extends U ? never : T`. if (falseType.flags & TypeFlags.Never && getActualTypeVariable(trueType) === getActualTypeVariable(checkType)) { - if (checkType.flags & TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true + if (checkType.flags & TypeFlags.Any || isTypeDefinitelyAssignableTo(checkType, extendsType)) { // Always true return getSimplifiedType(trueType, writing); } else if (isIntersectionEmpty(checkType, extendsType)) { // Always false @@ -13584,7 +13584,7 @@ namespace ts { } } else if (trueType.flags & TypeFlags.Never && getActualTypeVariable(falseType) === getActualTypeVariable(checkType)) { - if (!(checkType.flags & TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true + if (!(checkType.flags & TypeFlags.Any) && isTypeDefinitelyAssignableTo(checkType, extendsType)) { // Always true return neverType; } else if (checkType.flags & TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false @@ -13783,7 +13783,7 @@ namespace ts { // that has no constraint. This ensures that, for example, the type // type Foo = T extends { x: string } ? string : number // doesn't immediately resolve to 'string' instead of being deferred. - if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) { + if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown || isTypeDefinitelyAssignableTo(checkType, inferredExtendsType)) { result = instantiateTypeWithoutDepthIncrease(root.trueType, combinedMapper || mapper); break; } @@ -14513,14 +14513,6 @@ namespace ts { return !mapper ? makeUnaryTypeMapper(source, target) : makeCompositeTypeMapper(TypeMapKind.Merged, mapper, makeUnaryTypeMapper(source, target)); } - function getRestrictiveTypeParameter(tp: TypeParameter) { - return tp.constraint === unknownType ? tp : tp.restrictiveInstantiation || ( - tp.restrictiveInstantiation = createTypeParameter(tp.symbol), - (tp.restrictiveInstantiation as TypeParameter).constraint = unknownType, - tp.restrictiveInstantiation - ); - } - function cloneTypeParameter(typeParameter: TypeParameter): TypeParameter { const result = createTypeParameter(typeParameter.symbol); result.target = typeParameter; @@ -14883,7 +14875,7 @@ namespace ts { } else { const sub = instantiateType((type).substitute, mapper); - if (sub.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(maybeVariable), getRestrictiveInstantiation(sub))) { + if (sub.flags & TypeFlags.AnyOrUnknown || isTypeDefinitelyAssignableTo(maybeVariable, sub)) { return maybeVariable; } return sub; @@ -14897,23 +14889,6 @@ namespace ts { type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper)); } - function getRestrictiveInstantiation(type: Type) { - if (type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never)) { - return type; - } - if (type.restrictiveInstantiation) { - return type.restrictiveInstantiation; - } - type.restrictiveInstantiation = instantiateType(type, restrictiveMapper); - // We set the following so we don't attempt to set the restrictive instance of a restrictive instance - // which is redundant - we'll produce new type identities, but all type params have already been mapped. - // This also gives us a way to detect restrictive instances upon comparisons and _disable_ the "distributeive constraint" - // assignability check for them, which is distinctly unsafe, as once you have a restrctive instance, all the type parameters - // are constrained to `unknown` and produce tons of false positives/negatives! - type.restrictiveInstantiation.restrictiveInstantiation = type.restrictiveInstantiation; - return type.restrictiveInstantiation; - } - function instantiateIndexInfo(info: IndexInfo | undefined, mapper: TypeMapper): IndexInfo | undefined { return info && createIndexInfo(instantiateType(info.type, mapper), info.isReadonly, info.declaration); } @@ -15037,6 +15012,10 @@ namespace ts { return isTypeRelatedTo(source, target, assignableRelation); } + function isTypeDefinitelyAssignableTo(source: Type, target: Type): boolean { + return isTypeRelatedTo(source, target, definitelyAssignableRelation); + } + // An object type S is considered to be derived from an object type T if // S is a union type and every constituent of S is derived from T, // T is a union type and S is derived from at least one constituent of T, or @@ -15847,7 +15826,7 @@ namespace ts { if (s & TypeFlags.Undefined && (!strictNullChecks || t & (TypeFlags.Undefined | TypeFlags.Void))) return true; if (s & TypeFlags.Null && (!strictNullChecks || t & TypeFlags.Null)) return true; if (s & TypeFlags.Object && t & TypeFlags.NonPrimitive) return true; - if (relation === assignableRelation || relation === comparableRelation) { + if (relation === assignableRelation || relation === definitelyAssignableRelation || relation === comparableRelation) { if (s & TypeFlags.Any) return true; // Type number or any numeric literal type is assignable to any numeric enum type or any // numeric enum literal type. This rule exists for backwards compatibility reasons because @@ -16262,7 +16241,7 @@ namespace ts { // as we break down the _target_ union first, _then_ get the source constraint - so for every // member of the target, we attempt to find a match in the source. This avoids that in cases where // the target is exactly the constraint. - if (source.flags & TypeFlags.TypeParameter && getConstraintOfType(source) === target) { + if (relation !== definitelyAssignableRelation && source.flags & TypeFlags.TypeParameter && getConstraintOfType(source) === target) { return Ternary.True; } @@ -16482,7 +16461,7 @@ namespace ts { return false; // Disable excess property checks on JS literals to simulate having an implicit "index signature" - but only outside of noImplicitAny } const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); - if ((relation === assignableRelation || relation === comparableRelation) && + if ((relation === assignableRelation || relation === definitelyAssignableRelation || relation === comparableRelation) && (isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) { return false; } @@ -16867,31 +16846,33 @@ namespace ts { } } else if (target.flags & TypeFlags.Index) { - const targetType = (target as IndexType).type; - // A keyof S is related to a keyof T if T is related to S. - if (source.flags & TypeFlags.Index) { - if (result = isRelatedTo(targetType, (source).type, /*reportErrors*/ false)) { - return result; + if (relation !== definitelyAssignableRelation) { + const targetType = (target as IndexType).type; + // A keyof S is related to a keyof T if T is related to S. + if (source.flags & TypeFlags.Index) { + if (result = isRelatedTo(targetType, (source).type, /*reportErrors*/ false)) { + return result; + } } - } - if (isTupleType(targetType)) { - // An index type can have a tuple type target when the tuple type contains variadic elements. - // Check if the source is related to the known keys of the tuple type. - if (result = isRelatedTo(source, getKnownKeysOfTupleType(targetType), reportErrors)) { - return result; + if (isTupleType(targetType)) { + // An index type can have a tuple type target when the tuple type contains variadic elements. + // Check if the source is related to the known keys of the tuple type. + if (result = isRelatedTo(source, getKnownKeysOfTupleType(targetType), reportErrors)) { + return result; + } } - } - else { - // A type S is assignable to keyof T if S is assignable to keyof C, where C is the - // simplified form of T or, if T doesn't simplify, the constraint of T. - const constraint = getSimplifiedTypeOrConstraint(targetType); - if (constraint) { - // We require Ternary.True here such that circular constraints don't cause - // false positives. For example, given 'T extends { [K in keyof T]: string }', - // 'keyof T' has itself as its constraint and produces a Ternary.Maybe when - // related to other types. - if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).stringsOnly), reportErrors) === Ternary.True) { - return Ternary.True; + else { + // A type S is assignable to keyof T if S is assignable to keyof C, where C is the + // simplified form of T or, if T doesn't simplify, the constraint of T. + const constraint = getSimplifiedTypeOrConstraint(targetType); + if (constraint) { + // We require Ternary.True here such that circular constraints don't cause + // false positives. For example, given 'T extends { [K in keyof T]: string }', + // 'keyof T' has itself as its constraint and produces a Ternary.Maybe when + // related to other types. + if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).stringsOnly), reportErrors) === Ternary.True) { + return Ternary.True; + } } } } @@ -16957,7 +16938,7 @@ namespace ts { return result; } } - else { + else if (relation !== definitelyAssignableRelation) { const constraint = getConstraintOfType(source); if (!constraint || (source.flags & TypeFlags.TypeParameter && constraint.flags & TypeFlags.Any)) { // A type variable with no constraint is not related to the non-primitive object type. @@ -17010,7 +16991,7 @@ namespace ts { } } } - else { + else if (relation !== definitelyAssignableRelation) { // conditionals aren't related to one another via distributive constraint as it is much too inaccurate and allows way // more assignments than are desirable (since it maps the source check type to its constraint, it loses information) const distributiveConstraint = getConstraintOfDistributiveConditionalType(source);