Skip to content

Commit 3258d75

Browse files
authored
Reduce intersections of constrained type variables and primitive types (#56515)
1 parent f834133 commit 3258d75

16 files changed

+822
-39
lines changed

src/compiler/checker.ts

+81-3
Original file line numberDiff line numberDiff line change
@@ -16824,6 +16824,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1682416824
if (!(flags & TypeFlags.Never)) {
1682516825
includes |= flags & TypeFlags.IncludesMask;
1682616826
if (flags & TypeFlags.Instantiable) includes |= TypeFlags.IncludesInstantiable;
16827+
if (flags & TypeFlags.Intersection && getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) includes |= TypeFlags.IncludesConstrainedTypeVariable;
1682716828
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
1682816829
if (!strictNullChecks && flags & TypeFlags.Nullable) {
1682916830
if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType;
@@ -16968,6 +16969,49 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1696816969
}
1696916970
}
1697016971

16972+
function removeConstrainedTypeVariables(types: Type[]) {
16973+
const typeVariables: TypeVariable[] = [];
16974+
// First collect a list of the type variables occurring in constraining intersections.
16975+
for (const type of types) {
16976+
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
16977+
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
16978+
pushIfUnique(typeVariables, (type as IntersectionType).types[index]);
16979+
}
16980+
}
16981+
// For each type variable, check if the constraining intersections for that type variable fully
16982+
// cover the constraint of the type variable; if so, remove the constraining intersections and
16983+
// substitute the type variable.
16984+
for (const typeVariable of typeVariables) {
16985+
const primitives: Type[] = [];
16986+
// First collect the primitive types from the constraining intersections.
16987+
for (const type of types) {
16988+
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
16989+
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
16990+
if ((type as IntersectionType).types[index] === typeVariable) {
16991+
insertType(primitives, (type as IntersectionType).types[1 - index]);
16992+
}
16993+
}
16994+
}
16995+
// If every constituent in the type variable's constraint is covered by an intersection of the type
16996+
// variable and that constituent, remove those intersections and substitute the type variable.
16997+
const constraint = getBaseConstraintOfType(typeVariable)!;
16998+
if (everyType(constraint, t => containsType(primitives, t))) {
16999+
let i = types.length;
17000+
while (i > 0) {
17001+
i--;
17002+
const type = types[i];
17003+
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
17004+
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
17005+
if ((type as IntersectionType).types[index] === typeVariable && containsType(primitives, (type as IntersectionType).types[1 - index])) {
17006+
orderedRemoveItemAt(types, i);
17007+
}
17008+
}
17009+
}
17010+
insertType(types, typeVariable);
17011+
}
17012+
}
17013+
}
17014+
1697117015
function isNamedUnionType(type: Type) {
1697217016
return !!(type.flags & TypeFlags.Union && (type.aliasSymbol || (type as UnionType).origin));
1697317017
}
@@ -17042,6 +17086,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1704217086
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
1704317087
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
1704417088
}
17089+
if (includes & TypeFlags.IncludesConstrainedTypeVariable) {
17090+
removeConstrainedTypeVariables(typeSet);
17091+
}
1704517092
if (unionReduction === UnionReduction.Subtype) {
1704617093
typeSet = removeSubtypes(typeSet, !!(includes & TypeFlags.Object));
1704717094
if (!typeSet) {
@@ -17306,9 +17353,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1730617353
return true;
1730717354
}
1730817355

17309-
function createIntersectionType(types: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) {
17356+
function createIntersectionType(types: Type[], objectFlags: ObjectFlags, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) {
1731017357
const result = createType(TypeFlags.Intersection) as IntersectionType;
17311-
result.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
17358+
result.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
1731217359
result.types = types;
1731317360
result.aliasSymbol = aliasSymbol;
1731417361
result.aliasTypeArguments = aliasTypeArguments;
@@ -17329,6 +17376,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1732917376
const typeMembershipMap = new Map<string, Type>();
1733017377
const includes = addTypesToIntersection(typeMembershipMap, 0 as TypeFlags, types);
1733117378
const typeSet: Type[] = arrayFrom(typeMembershipMap.values());
17379+
let objectFlags = ObjectFlags.None;
1733217380
// An intersection type is considered empty if it contains
1733317381
// the type never, or
1733417382
// more than one unit type or,
@@ -17380,6 +17428,36 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1738017428
if (typeSet.length === 1) {
1738117429
return typeSet[0];
1738217430
}
17431+
if (typeSet.length === 2) {
17432+
const typeVarIndex = typeSet[0].flags & TypeFlags.TypeVariable ? 0 : 1;
17433+
const typeVariable = typeSet[typeVarIndex];
17434+
const primitiveType = typeSet[1 - typeVarIndex];
17435+
if (typeVariable.flags & TypeFlags.TypeVariable && (primitiveType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || includes & TypeFlags.IncludesEmptyObject)) {
17436+
// We have an intersection T & P or P & T, where T is a type variable and P is a primitive type, the object type, or {}.
17437+
const constraint = getBaseConstraintOfType(typeVariable);
17438+
// Check that T's constraint is similarly composed of primitive types, the object type, or {}.
17439+
if (constraint && everyType(constraint, t => !!(t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) || isEmptyAnonymousObjectType(t))) {
17440+
// If T's constraint is a subtype of P, simply return T. For example, given `T extends "a" | "b"`,
17441+
// the intersection `T & string` reduces to just T.
17442+
if (isTypeStrictSubtypeOf(constraint, primitiveType)) {
17443+
return typeVariable;
17444+
}
17445+
if (!(constraint.flags & TypeFlags.Union && someType(constraint, c => isTypeStrictSubtypeOf(c, primitiveType)))) {
17446+
// No constituent of T's constraint is a subtype of P. If P is also not a subtype of T's constraint,
17447+
// then the constraint and P are unrelated, and the intersection reduces to never. For example, given
17448+
// `T extends "a" | "b"`, the intersection `T & number` reduces to never.
17449+
if (!isTypeStrictSubtypeOf(primitiveType, constraint)) {
17450+
return neverType;
17451+
}
17452+
}
17453+
// Some constituent of T's constraint is a subtype of P, or P is a subtype of T's constraint. Thus,
17454+
// the intersection further constrains the type variable. For example, given `T extends string | number`,
17455+
// the intersection `T & "a"` is marked as a constrained type variable. Likewise, given `T extends "a" | 1`,
17456+
// the intersection `T & number` is marked as a constrained type variable.
17457+
objectFlags = ObjectFlags.IsConstrainedTypeVariable;
17458+
}
17459+
}
17460+
}
1738317461
const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments);
1738417462
let result = intersectionTypes.get(id);
1738517463
if (!result) {
@@ -17415,7 +17493,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1741517493
}
1741617494
}
1741717495
else {
17418-
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
17496+
result = createIntersectionType(typeSet, objectFlags, aliasSymbol, aliasTypeArguments);
1741917497
}
1742017498
intersectionTypes.set(id, result);
1742117499
}

src/compiler/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6153,6 +6153,8 @@ export const enum TypeFlags {
61536153
/** @internal */
61546154
IncludesInstantiable = Substitution,
61556155
/** @internal */
6156+
IncludesConstrainedTypeVariable = StringMapping,
6157+
/** @internal */
61566158
NotPrimitiveUnion = Any | Unknown | Void | Never | Object | Intersection | IncludesInstantiable,
61576159
}
61586160

@@ -6313,6 +6315,8 @@ export const enum ObjectFlags {
63136315
IsNeverIntersectionComputed = 1 << 24, // IsNeverLike flag has been computed
63146316
/** @internal */
63156317
IsNeverIntersection = 1 << 25, // Intersection reduces to never
6318+
/** @internal */
6319+
IsConstrainedTypeVariable = 1 << 26, // T & C, where T's constraint and C are primitives, object, or {}
63166320
}
63176321

63186322
/** @internal */

tests/baselines/reference/conditionalTypes1.errors.txt

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ conditionalTypes1.ts(288,43): error TS2322: Type 'T95<U>' is not assignable to t
8888
!!! error TS2322: Type 'T' is not assignable to type 'NonNullable<T>'.
8989
!!! error TS2322: Type 'T' is not assignable to type '{}'.
9090
!!! related TS2208 conditionalTypes1.ts:10:13: This type parameter might need an `extends {}` constraint.
91+
!!! related TS2208 conditionalTypes1.ts:10:13: This type parameter might need an `extends NonNullable<T>` constraint.
9192
}
9293

9394
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {

tests/baselines/reference/inKeywordAndUnknown.types

+5-5
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ function f5<T>(x: T & {}) {
138138

139139
function f6<T extends {}>(x: T & {}) {
140140
>f6 : <T extends {}>(x: T & {}) => boolean
141-
>x : T & {}
141+
>x : T
142142

143143
return x instanceof Object && 'a' in x;
144144
>x instanceof Object && 'a' in x : boolean
145145
>x instanceof Object : boolean
146-
>x : T & {}
146+
>x : T
147147
>Object : ObjectConstructor
148148
>'a' in x : boolean
149149
>'a' : "a"
@@ -152,15 +152,15 @@ function f6<T extends {}>(x: T & {}) {
152152

153153
function f7<T extends object>(x: T & {}) {
154154
>f7 : <T extends object>(x: T & {}) => boolean
155-
>x : T & {}
155+
>x : T
156156

157157
return x instanceof Object && 'a' in x;
158158
>x instanceof Object && 'a' in x : boolean
159159
>x instanceof Object : boolean
160-
>x : T & {}
160+
>x : T
161161
>Object : ObjectConstructor
162162
>'a' in x : boolean
163163
>'a' : "a"
164-
>x : T & {}
164+
>x : T
165165
}
166166

tests/baselines/reference/inKeywordTypeguard(strict=false).types

+2-2
Original file line numberDiff line numberDiff line change
@@ -1078,12 +1078,12 @@ function isHTMLTable<T extends object | null>(table: T): boolean {
10781078
const f = <P extends object>(a: P & {}) => {
10791079
>f : <P extends object>(a: P & {}) => void
10801080
><P extends object>(a: P & {}) => { "foo" in a;} : <P extends object>(a: P & {}) => void
1081-
>a : P & {}
1081+
>a : P
10821082

10831083
"foo" in a;
10841084
>"foo" in a : boolean
10851085
>"foo" : "foo"
1086-
>a : P & {}
1086+
>a : P
10871087

10881088
};
10891089

tests/baselines/reference/inKeywordTypeguard(strict=true).types

+2-2
Original file line numberDiff line numberDiff line change
@@ -1078,12 +1078,12 @@ function isHTMLTable<T extends object | null>(table: T): boolean {
10781078
const f = <P extends object>(a: P & {}) => {
10791079
>f : <P extends object>(a: P & {}) => void
10801080
><P extends object>(a: P & {}) => { "foo" in a;} : <P extends object>(a: P & {}) => void
1081-
>a : P & {}
1081+
>a : P
10821082

10831083
"foo" in a;
10841084
>"foo" in a : boolean
10851085
>"foo" : "foo"
1086-
>a : P & {}
1086+
>a : P
10871087

10881088
};
10891089

tests/baselines/reference/indexSignatures1.errors.txt

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ indexSignatures1.ts(73,5): error TS2374: Duplicate index signature for type '`fo
1515
indexSignatures1.ts(81,5): error TS2413: '`a${string}a`' index type '"c"' is not assignable to '`${string}a`' index type '"b"'.
1616
indexSignatures1.ts(81,5): error TS2413: '`a${string}a`' index type '"c"' is not assignable to '`a${string}`' index type '"a"'.
1717
indexSignatures1.ts(87,6): error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
18+
indexSignatures1.ts(88,5): error TS2374: Duplicate index signature for type 'T'.
1819
indexSignatures1.ts(88,6): error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
1920
indexSignatures1.ts(89,6): error TS1268: An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
21+
indexSignatures1.ts(90,5): error TS2374: Duplicate index signature for type 'T'.
2022
indexSignatures1.ts(90,6): error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
2123
indexSignatures1.ts(117,1): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'I1'.
2224
No index signature with a parameter of type 'string' was found on type 'I1'.
@@ -69,7 +71,7 @@ indexSignatures1.ts(289,7): error TS2322: Type 'number' is not assignable to typ
6971
indexSignatures1.ts(312,43): error TS2353: Object literal may only specify known properties, and '[sym]' does not exist in type '{ [key: number]: string; }'.
7072

7173

72-
==== indexSignatures1.ts (50 errors) ====
74+
==== indexSignatures1.ts (52 errors) ====
7375
// Symbol index signature checking
7476

7577
const sym = Symbol();
@@ -188,12 +190,16 @@ indexSignatures1.ts(312,43): error TS2353: Object literal may only specify known
188190
~~~
189191
!!! error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
190192
[key: T | number]: string; // Error
193+
~~~~~~~~~~~~~~~~~~~~~~~~~~
194+
!!! error TS2374: Duplicate index signature for type 'T'.
191195
~~~
192196
!!! error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
193197
[key: Error]: string; // Error
194198
~~~
195199
!!! error TS1268: An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
196200
[key: T & string]: string; // Error
201+
~~~~~~~~~~~~~~~~~~~~~~~~~~
202+
!!! error TS2374: Duplicate index signature for type 'T'.
197203
~~~
198204
!!! error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
199205
}

tests/baselines/reference/indexSignatures1.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ type Invalid<T extends string> = {
261261
>key : Error
262262

263263
[key: T & string]: string; // Error
264-
>key : T & string
264+
>key : T
265265
}
266266

267267
// Intersections in index signatures

tests/baselines/reference/intersectionWithUnionConstraint.types

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type T1 = (string | number | undefined) & (string | null | undefined); // strin
4545

4646
function f3<T extends string | number | undefined>(x: T & (number | object | undefined)) {
4747
>f3 : <T extends string | number | undefined>(x: T & (number | object | undefined)) => void
48-
>x : T & (number | object | undefined)
48+
>x : (T & undefined) | (T & number)
4949

5050
const y: number | undefined = x;
5151
>y : number | undefined
@@ -54,7 +54,7 @@ function f3<T extends string | number | undefined>(x: T & (number | object | und
5454

5555
function f4<T extends string | number>(x: T & (number | object)) {
5656
>f4 : <T extends string | number>(x: T & (number | object)) => void
57-
>x : T & (number | object)
57+
>x : T & number
5858

5959
const y: number = x;
6060
>y : number

tests/baselines/reference/spreadObjectOrFalsy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ declare function f5<S, T extends undefined>(a: S | T): S | T;
113113
declare function f6<T extends object | undefined>(a: T): T;
114114
declare function g1<T extends {}, A extends {
115115
z: (T | undefined) & T;
116-
}>(a: A): T | (undefined & T);
116+
}>(a: A): T;
117117
interface DatafulFoo<T> {
118118
data: T;
119119
}

tests/baselines/reference/spreadObjectOrFalsy.types

+7-7
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,19 @@ function f6<T extends object | undefined>(a: T) {
5858
// Repro from #46976
5959

6060
function g1<T extends {}, A extends { z: (T | undefined) & T }>(a: A) {
61-
>g1 : <T extends {}, A extends { z: (T | undefined) & T; }>(a: A) => T | (undefined & T)
62-
>z : T | (undefined & T)
61+
>g1 : <T extends {}, A extends { z: (T | undefined) & T; }>(a: A) => T
62+
>z : T
6363
>a : A
6464

6565
const { z } = a;
66-
>z : T | (undefined & T)
66+
>z : T
6767
>a : A
6868

6969
return {
70-
>{ ...z } : T | (undefined & T)
70+
>{ ...z } : T
7171

7272
...z
73-
>z : T | (undefined & T)
73+
>z : T
7474

7575
};
7676
}
@@ -100,9 +100,9 @@ class Foo<T extends string> {
100100
this.data.toLocaleLowerCase();
101101
>this.data.toLocaleLowerCase() : string
102102
>this.data.toLocaleLowerCase : (locales?: string | string[] | undefined) => string
103-
>this.data : T | (undefined & T)
103+
>this.data : T
104104
>this : this & DatafulFoo<T>
105-
>data : T | (undefined & T)
105+
>data : T
106106
>toLocaleLowerCase : (locales?: string | string[] | undefined) => string
107107
}
108108
}

0 commit comments

Comments
 (0)