Skip to content
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

Reduce intersections of constrained type variables and primitive types #56515

Merged
merged 3 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16794,6 +16794,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!(flags & TypeFlags.Never)) {
includes |= flags & TypeFlags.IncludesMask;
if (flags & TypeFlags.Instantiable) includes |= TypeFlags.IncludesInstantiable;
if (flags & TypeFlags.Intersection && getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) includes |= TypeFlags.IncludesConstrainedTypeVariable;
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
if (!strictNullChecks && flags & TypeFlags.Nullable) {
if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType;
Expand Down Expand Up @@ -16938,6 +16939,49 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function removeConstrainedTypeVariables(types: Type[]) {
const typeVariables: TypeVariable[] = [];
// First collect a list of the type variables occurring in constraining intersections.
for (const type of types) {
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
pushIfUnique(typeVariables, (type as IntersectionType).types[index]);
}
}
// For each type variable, check if the constraining intersections for that type variable fully
// cover the constraint of the type variable; if so, remove the constraining intersections and
// substitute the type variable.
for (const typeVariable of typeVariables) {
const primitives: Type[] = [];
// First collect the primitive types from the constraining intersections.
for (const type of types) {
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
if ((type as IntersectionType).types[index] === typeVariable) {
insertType(primitives, (type as IntersectionType).types[1 - index]);
}
}
}
// If every constituent in the type variable's constraint is covered by an intersection of the type
// variable and that constituent, remove those intersections and substitute the type variable.
const constraint = getBaseConstraintOfType(typeVariable)!;
if (everyType(constraint, t => containsType(primitives, t))) {
let i = types.length;
while (i > 0) {
i--;
const type = types[i];
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
if ((type as IntersectionType).types[index] === typeVariable && containsType(primitives, (type as IntersectionType).types[1 - index])) {
orderedRemoveItemAt(types, i);
}
}
}
insertType(types, typeVariable);
}
}
}

function isNamedUnionType(type: Type) {
return !!(type.flags & TypeFlags.Union && (type.aliasSymbol || (type as UnionType).origin));
}
Expand Down Expand Up @@ -17012,6 +17056,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
}
if (includes & TypeFlags.IncludesConstrainedTypeVariable) {
removeConstrainedTypeVariables(typeSet);
}
if (unionReduction === UnionReduction.Subtype) {
typeSet = removeSubtypes(typeSet, !!(includes & TypeFlags.Object));
if (!typeSet) {
Expand Down Expand Up @@ -17276,9 +17323,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return true;
}

function createIntersectionType(types: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) {
function createIntersectionType(types: Type[], objectFlags: ObjectFlags, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) {
const result = createType(TypeFlags.Intersection) as IntersectionType;
result.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
result.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
result.types = types;
result.aliasSymbol = aliasSymbol;
result.aliasTypeArguments = aliasTypeArguments;
Expand All @@ -17299,6 +17346,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const typeMembershipMap = new Map<string, Type>();
const includes = addTypesToIntersection(typeMembershipMap, 0 as TypeFlags, types);
const typeSet: Type[] = arrayFrom(typeMembershipMap.values());
let objectFlags = ObjectFlags.None;
// An intersection type is considered empty if it contains
// the type never, or
// more than one unit type or,
Expand Down Expand Up @@ -17350,6 +17398,36 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (typeSet.length === 1) {
return typeSet[0];
}
if (typeSet.length === 2) {
const typeVarIndex = typeSet[0].flags & TypeFlags.TypeVariable ? 0 : 1;
const typeVariable = typeSet[typeVarIndex];
const primitiveType = typeSet[1 - typeVarIndex];
if (typeVariable.flags & TypeFlags.TypeVariable && (primitiveType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || includes & TypeFlags.IncludesEmptyObject)) {
// 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 {}.
const constraint = getBaseConstraintOfType(typeVariable);
// Check that T's constraint is similarly composed of primitive types, the object type, or {}.
if (constraint && everyType(constraint, t => !!(t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) || isEmptyAnonymousObjectType(t))) {
// If T's constraint is a subtype of P, simply return T. For example, given `T extends "a" | "b"`,
// the intersection `T & string` reduces to just T.
if (isTypeStrictSubtypeOf(constraint, primitiveType)) {
return typeVariable;
}
if (!(constraint.flags & TypeFlags.Union && someType(constraint, c => isTypeStrictSubtypeOf(c, primitiveType)))) {
// No constituent of T's constraint is a subtype of P. If P is also not a subtype of T's constraint,
// then the constraint and P are unrelated, and the intersection reduces to never. For example, given
// `T extends "a" | "b"`, the intersection `T & number` reduces to never.
if (!isTypeStrictSubtypeOf(primitiveType, constraint)) {
return neverType;
}
}
// Some constituent of T's constraint is a subtype of P, or P is a subtype of T's constraint. Thus,
// the intersection further constrains the type variable. For example, given `T extends string | number`,
// the intersection `T & "a"` is marked as a constrained type variable. Likewise, given `T extends "a" | 1`,
// the intersection `T & number` is marked as a constrained type variable.
objectFlags = ObjectFlags.IsConstrainedTypeVariable;
}
}
}
const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments);
let result = intersectionTypes.get(id);
if (!result) {
Expand Down Expand Up @@ -17385,7 +17463,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
else {
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
result = createIntersectionType(typeSet, objectFlags, aliasSymbol, aliasTypeArguments);
}
intersectionTypes.set(id, result);
}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6153,6 +6153,8 @@ export const enum TypeFlags {
/** @internal */
IncludesInstantiable = Substitution,
/** @internal */
IncludesConstrainedTypeVariable = StringMapping,
/** @internal */
NotPrimitiveUnion = Any | Unknown | Void | Never | Object | Intersection | IncludesInstantiable,
}

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

/** @internal */
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/conditionalTypes1.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ conditionalTypes1.ts(288,43): error TS2322: Type 'T95<U>' is not assignable to t
!!! error TS2322: Type 'T' is not assignable to type 'NonNullable<T>'.
!!! error TS2322: Type 'T' is not assignable to type '{}'.
!!! related TS2208 conditionalTypes1.ts:10:13: This type parameter might need an `extends {}` constraint.
!!! related TS2208 conditionalTypes1.ts:10:13: This type parameter might need an `extends NonNullable<T>` constraint.
}

function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
Expand Down
10 changes: 5 additions & 5 deletions tests/baselines/reference/inKeywordAndUnknown.types
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ function f5<T>(x: T & {}) {

function f6<T extends {}>(x: T & {}) {
>f6 : <T extends {}>(x: T & {}) => boolean
>x : T & {}
>x : T

return x instanceof Object && 'a' in x;
>x instanceof Object && 'a' in x : boolean
>x instanceof Object : boolean
>x : T & {}
>x : T
>Object : ObjectConstructor
>'a' in x : boolean
>'a' : "a"
Expand All @@ -152,15 +152,15 @@ function f6<T extends {}>(x: T & {}) {

function f7<T extends object>(x: T & {}) {
>f7 : <T extends object>(x: T & {}) => boolean
>x : T & {}
>x : T

return x instanceof Object && 'a' in x;
>x instanceof Object && 'a' in x : boolean
>x instanceof Object : boolean
>x : T & {}
>x : T
>Object : ObjectConstructor
>'a' in x : boolean
>'a' : "a"
>x : T & {}
>x : T
}

Original file line number Diff line number Diff line change
Expand Up @@ -1078,12 +1078,12 @@ function isHTMLTable<T extends object | null>(table: T): boolean {
const f = <P extends object>(a: P & {}) => {
>f : <P extends object>(a: P & {}) => void
><P extends object>(a: P & {}) => { "foo" in a;} : <P extends object>(a: P & {}) => void
>a : P & {}
>a : P

"foo" in a;
>"foo" in a : boolean
>"foo" : "foo"
>a : P & {}
>a : P

};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1078,12 +1078,12 @@ function isHTMLTable<T extends object | null>(table: T): boolean {
const f = <P extends object>(a: P & {}) => {
>f : <P extends object>(a: P & {}) => void
><P extends object>(a: P & {}) => { "foo" in a;} : <P extends object>(a: P & {}) => void
>a : P & {}
>a : P

"foo" in a;
>"foo" in a : boolean
>"foo" : "foo"
>a : P & {}
>a : P

};

Expand Down
8 changes: 7 additions & 1 deletion tests/baselines/reference/indexSignatures1.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ indexSignatures1.ts(73,5): error TS2374: Duplicate index signature for type '`fo
indexSignatures1.ts(81,5): error TS2413: '`a${string}a`' index type '"c"' is not assignable to '`${string}a`' index type '"b"'.
indexSignatures1.ts(81,5): error TS2413: '`a${string}a`' index type '"c"' is not assignable to '`a${string}`' index type '"a"'.
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.
indexSignatures1.ts(88,5): error TS2374: Duplicate index signature for type 'T'.
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.
indexSignatures1.ts(89,6): error TS1268: An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
indexSignatures1.ts(90,5): error TS2374: Duplicate index signature for type 'T'.
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.
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'.
No index signature with a parameter of type 'string' was found on type 'I1'.
Expand Down Expand Up @@ -69,7 +71,7 @@ indexSignatures1.ts(289,7): error TS2322: Type 'number' is not assignable to typ
indexSignatures1.ts(312,43): error TS2353: Object literal may only specify known properties, and '[sym]' does not exist in type '{ [key: number]: string; }'.


==== indexSignatures1.ts (50 errors) ====
==== indexSignatures1.ts (52 errors) ====
// Symbol index signature checking

const sym = Symbol();
Expand Down Expand Up @@ -188,12 +190,16 @@ indexSignatures1.ts(312,43): error TS2353: Object literal may only specify known
~~~
!!! error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
[key: T | number]: string; // Error
~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2374: Duplicate index signature for type 'T'.
~~~
!!! error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
[key: Error]: string; // Error
~~~
!!! error TS1268: An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
[key: T & string]: string; // Error
~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2374: Duplicate index signature for type 'T'.
~~~
!!! error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
}
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/indexSignatures1.types
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ type Invalid<T extends string> = {
>key : Error

[key: T & string]: string; // Error
>key : T & string
>key : T
}

// Intersections in index signatures
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type T1 = (string | number | undefined) & (string | null | undefined); // strin

function f3<T extends string | number | undefined>(x: T & (number | object | undefined)) {
>f3 : <T extends string | number | undefined>(x: T & (number | object | undefined)) => void
>x : T & (number | object | undefined)
>x : (T & undefined) | (T & number)

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

function f4<T extends string | number>(x: T & (number | object)) {
>f4 : <T extends string | number>(x: T & (number | object)) => void
>x : T & (number | object)
>x : T & number

const y: number = x;
>y : number
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/spreadObjectOrFalsy.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ declare function f5<S, T extends undefined>(a: S | T): S | T;
declare function f6<T extends object | undefined>(a: T): T;
declare function g1<T extends {}, A extends {
z: (T | undefined) & T;
}>(a: A): T | (undefined & T);
}>(a: A): T;
interface DatafulFoo<T> {
data: T;
}
Expand Down
14 changes: 7 additions & 7 deletions tests/baselines/reference/spreadObjectOrFalsy.types
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@ function f6<T extends object | undefined>(a: T) {
// Repro from #46976

function g1<T extends {}, A extends { z: (T | undefined) & T }>(a: A) {
>g1 : <T extends {}, A extends { z: (T | undefined) & T; }>(a: A) => T | (undefined & T)
>z : T | (undefined & T)
>g1 : <T extends {}, A extends { z: (T | undefined) & T; }>(a: A) => T
>z : T
>a : A

const { z } = a;
>z : T | (undefined & T)
>z : T
>a : A

return {
>{ ...z } : T | (undefined & T)
>{ ...z } : T

...z
>z : T | (undefined & T)
>z : T

};
}
Expand Down Expand Up @@ -100,9 +100,9 @@ class Foo<T extends string> {
this.data.toLocaleLowerCase();
>this.data.toLocaleLowerCase() : string
>this.data.toLocaleLowerCase : (locales?: string | string[] | undefined) => string
>this.data : T | (undefined & T)
>this.data : T
>this : this & DatafulFoo<T>
>data : T | (undefined & T)
>data : T
>toLocaleLowerCase : (locales?: string | string[] | undefined) => string
}
}
Expand Down
Loading
Loading