diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9fc854515ee96..32eb3b5bd3c22 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -774,7 +774,6 @@ namespace ts { const numberOrBigIntType = getUnionType([numberType, bigintType]); const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType, nullType, undefinedType]) as UnionType; - 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); @@ -973,6 +972,7 @@ namespace ts { const subtypeRelation = new Map(); const strictSubtypeRelation = new Map(); const assignableRelation = new Map(); + const definitelyAssignableRelation = new Map(); const comparableRelation = new Map(); const identityRelation = new Map(); const enumRelation = new Map(); @@ -14116,7 +14116,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 @@ -14124,7 +14124,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 @@ -14331,7 +14331,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 = instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper); break; } @@ -15069,14 +15069,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; @@ -15441,7 +15433,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; @@ -15455,23 +15447,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); } @@ -15595,6 +15570,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 @@ -16403,7 +16382,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 @@ -16822,7 +16801,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; } @@ -17042,7 +17021,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; } @@ -17449,31 +17428,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; + } } } } @@ -17559,7 +17540,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. @@ -17641,7 +17622,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); diff --git a/tests/baselines/reference/conditionalTypes2.errors.txt b/tests/baselines/reference/conditionalTypes2.errors.txt index 1f92090e2e137..770a6a2419f93 100644 --- a/tests/baselines/reference/conditionalTypes2.errors.txt +++ b/tests/baselines/reference/conditionalTypes2.errors.txt @@ -326,4 +326,10 @@ tests/cases/conformance/types/conditional/conditionalTypes2.ts(75,12): error TS2 declare function gg(f: (x: Foo3) => void): void; type Foo3 = T extends number ? { n: T } : { x: T }; gg(ff); + + // Repro from 39364 + + type StringOrNumber any> = [ReturnType] extends [string] ? string : number; + + type Test = StringOrNumber<() => number>; // number \ No newline at end of file diff --git a/tests/baselines/reference/conditionalTypes2.js b/tests/baselines/reference/conditionalTypes2.js index ac2cb7465f72d..46243626a0f95 100644 --- a/tests/baselines/reference/conditionalTypes2.js +++ b/tests/baselines/reference/conditionalTypes2.js @@ -237,6 +237,12 @@ declare function ff(x: Foo3): void; declare function gg(f: (x: Foo3) => void): void; type Foo3 = T extends number ? { n: T } : { x: T }; gg(ff); + +// Repro from 39364 + +type StringOrNumber any> = [ReturnType] extends [string] ? string : number; + +type Test = StringOrNumber<() => number>; // number //// [conditionalTypes2.js] @@ -477,3 +483,5 @@ declare type Foo3 = T extends number ? { } : { x: T; }; +declare type StringOrNumber any> = [ReturnType] extends [string] ? string : number; +declare type Test = StringOrNumber<() => number>; diff --git a/tests/baselines/reference/conditionalTypes2.symbols b/tests/baselines/reference/conditionalTypes2.symbols index 9f980b3d0cec3..150d45ab432ca 100644 --- a/tests/baselines/reference/conditionalTypes2.symbols +++ b/tests/baselines/reference/conditionalTypes2.symbols @@ -841,3 +841,15 @@ gg(ff); >gg : Symbol(gg, Decl(conditionalTypes2.ts, 234, 43)) >ff : Symbol(ff, Decl(conditionalTypes2.ts, 230, 2)) +// Repro from 39364 + +type StringOrNumber any> = [ReturnType] extends [string] ? string : number; +>StringOrNumber : Symbol(StringOrNumber, Decl(conditionalTypes2.ts, 237, 7)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 241, 20)) +>ReturnType : Symbol(ReturnType, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 241, 20)) + +type Test = StringOrNumber<() => number>; // number +>Test : Symbol(Test, Decl(conditionalTypes2.ts, 241, 94)) +>StringOrNumber : Symbol(StringOrNumber, Decl(conditionalTypes2.ts, 237, 7)) + diff --git a/tests/baselines/reference/conditionalTypes2.types b/tests/baselines/reference/conditionalTypes2.types index d7487a1111007..906ed795b24ab 100644 --- a/tests/baselines/reference/conditionalTypes2.types +++ b/tests/baselines/reference/conditionalTypes2.types @@ -527,3 +527,11 @@ gg(ff); >gg : (f: (x: Foo3) => void) => void >ff : (x: { x: string; }) => void +// Repro from 39364 + +type StringOrNumber any> = [ReturnType] extends [string] ? string : number; +>StringOrNumber : StringOrNumber + +type Test = StringOrNumber<() => number>; // number +>Test : number + diff --git a/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.errors.txt b/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.errors.txt index 4dcf8530b019d..8ca5c51ea2dd4 100644 --- a/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.errors.txt +++ b/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.errors.txt @@ -25,9 +25,10 @@ tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterInd tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterIndirectlyConstrainedToItself.ts(16,47): error TS2313: Type parameter 'V' has a circular constraint. tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterIndirectlyConstrainedToItself.ts(18,32): error TS2313: Type parameter 'T' has a circular constraint. tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterIndirectlyConstrainedToItself.ts(18,45): error TS2313: Type parameter 'V' has a circular constraint. +tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterIndirectlyConstrainedToItself.ts(23,24): error TS2313: Type parameter 'S' has a circular constraint. -==== tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterIndirectlyConstrainedToItself.ts (27 errors) ==== +==== tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterIndirectlyConstrainedToItself.ts (28 errors) ==== class C { } ~ !!! error TS2313: Type parameter 'U' has a circular constraint. @@ -105,4 +106,6 @@ tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterInd type Foo = [T] extends [number] ? {} : {}; function foo>() {} + ~~~~~~ +!!! error TS2313: Type parameter 'S' has a circular constraint. \ No newline at end of file diff --git a/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.types b/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.types index 637cb583d2e62..c2b20688fb5cb 100644 --- a/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.types +++ b/tests/baselines/reference/typeParameterIndirectlyConstrainedToItself.types @@ -38,5 +38,5 @@ type Foo = [T] extends [number] ? {} : {}; >Foo : Foo function foo>() {} ->foo : >() => void +>foo : () => void diff --git a/tests/cases/conformance/types/conditional/conditionalTypes2.ts b/tests/cases/conformance/types/conditional/conditionalTypes2.ts index ea30ec89fbfa8..40e8348a8ad2d 100644 --- a/tests/cases/conformance/types/conditional/conditionalTypes2.ts +++ b/tests/cases/conformance/types/conditional/conditionalTypes2.ts @@ -239,3 +239,9 @@ declare function ff(x: Foo3): void; declare function gg(f: (x: Foo3) => void): void; type Foo3 = T extends number ? { n: T } : { x: T }; gg(ff); + +// Repro from 39364 + +type StringOrNumber any> = [ReturnType] extends [string] ? string : number; + +type Test = StringOrNumber<() => number>; // number