Skip to content

Reintroduce definitelyAssignableRelation and use with conditional types #39577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
97 changes: 39 additions & 58 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(<TypeParameter>t) : t);
const permissiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? wildcardType : t);

const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
Expand Down Expand Up @@ -973,6 +972,7 @@ namespace ts {
const subtypeRelation = new Map<string, RelationComparisonResult>();
const strictSubtypeRelation = new Map<string, RelationComparisonResult>();
const assignableRelation = new Map<string, RelationComparisonResult>();
const definitelyAssignableRelation = new Map<string, RelationComparisonResult>();
const comparableRelation = new Map<string, RelationComparisonResult>();
const identityRelation = new Map<string, RelationComparisonResult>();
const enumRelation = new Map<string, RelationComparisonResult>();
Expand Down Expand Up @@ -14116,15 +14116,15 @@ 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
return neverType;
}
}
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
Expand Down Expand Up @@ -14331,7 +14331,7 @@ namespace ts {
// that has no constraint. This ensures that, for example, the type
// type Foo<T extends { x: any }> = 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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -15441,7 +15433,7 @@ namespace ts {
}
else {
const sub = instantiateType((<SubstitutionType>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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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, (<IndexType>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, (<IndexType>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;
}
}
}
}
Expand Down Expand Up @@ -17559,7 +17540,7 @@ namespace ts {
return result;
}
}
else {
else if (relation !== definitelyAssignableRelation) {
const constraint = getConstraintOfType(<TypeVariable>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.
Expand Down Expand Up @@ -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(<ConditionalType>source);
Expand Down
6 changes: 6 additions & 0 deletions tests/baselines/reference/conditionalTypes2.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,10 @@ tests/cases/conformance/types/conditional/conditionalTypes2.ts(75,12): error TS2
declare function gg<T>(f: (x: Foo3<T>) => void): void;
type Foo3<T> = T extends number ? { n: T } : { x: T };
gg(ff);

// Repro from 39364

type StringOrNumber<T extends () => any> = [ReturnType<T>] extends [string] ? string : number;

type Test = StringOrNumber<() => number>; // number

8 changes: 8 additions & 0 deletions tests/baselines/reference/conditionalTypes2.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ declare function ff(x: Foo3<string>): void;
declare function gg<T>(f: (x: Foo3<T>) => void): void;
type Foo3<T> = T extends number ? { n: T } : { x: T };
gg(ff);

// Repro from 39364

type StringOrNumber<T extends () => any> = [ReturnType<T>] extends [string] ? string : number;

type Test = StringOrNumber<() => number>; // number


//// [conditionalTypes2.js]
Expand Down Expand Up @@ -477,3 +483,5 @@ declare type Foo3<T> = T extends number ? {
} : {
x: T;
};
declare type StringOrNumber<T extends () => any> = [ReturnType<T>] extends [string] ? string : number;
declare type Test = StringOrNumber<() => number>;
12 changes: 12 additions & 0 deletions tests/baselines/reference/conditionalTypes2.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends () => any> = [ReturnType<T>] 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))

8 changes: 8 additions & 0 deletions tests/baselines/reference/conditionalTypes2.types
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,11 @@ gg(ff);
>gg : <T>(f: (x: Foo3<T>) => void) => void
>ff : (x: { x: string; }) => void

// Repro from 39364

type StringOrNumber<T extends () => any> = [ReturnType<T>] extends [string] ? string : number;
>StringOrNumber : StringOrNumber<T>

type Test = StringOrNumber<() => number>; // number
>Test : number

Original file line number Diff line number Diff line change
Expand Up @@ -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<U extends T, T extends U> { }
~
!!! error TS2313: Type parameter 'U' has a circular constraint.
Expand Down Expand Up @@ -105,4 +106,6 @@ tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterInd

type Foo<T> = [T] extends [number] ? {} : {};
function foo<S extends Foo<S>>() {}
~~~~~~
!!! error TS2313: Type parameter 'S' has a circular constraint.

Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ type Foo<T> = [T] extends [number] ? {} : {};
>Foo : Foo<T>

function foo<S extends Foo<S>>() {}
>foo : <S extends Foo<S>>() => void
>foo : <S>() => void

Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,9 @@ declare function ff(x: Foo3<string>): void;
declare function gg<T>(f: (x: Foo3<T>) => void): void;
type Foo3<T> = T extends number ? { n: T } : { x: T };
gg(ff);

// Repro from 39364

type StringOrNumber<T extends () => any> = [ReturnType<T>] extends [string] ? string : number;

type Test = StringOrNumber<() => number>; // number