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

Merge trivially mergeable intersection types for identity comparison #60726

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
51 changes: 47 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14485,17 +14485,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getAugmentedPropertiesOfType(unionType);
}

const props = getMembersOfUnionOrIntersection(unionType as UnionType);
return arrayFrom(props.values());
}

function getMembersOfUnionOrIntersection(type: UnionOrIntersectionType): SymbolTable {
const props = createSymbolTable();
for (const memberType of types) {
for (const memberType of type.types) {
for (const { escapedName } of getAugmentedPropertiesOfType(memberType)) {
if (!props.has(escapedName)) {
const prop = createUnionOrIntersectionProperty(unionType as UnionType, escapedName);
const prop = createUnionOrIntersectionProperty(type, escapedName);
// May be undefined if the property is private
if (prop) props.set(escapedName, prop);
}
}
}
return arrayFrom(props.values());
return props;
}

function getConstraintOfType(type: InstantiableType | UnionOrIntersectionType): Type | undefined {
Expand Down Expand Up @@ -21730,6 +21735,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
hasSubstitution ||= isNarrowingSubstitutionType(t); // This avoids displaying error messages with types like `T & T` when narrowing a return type
if (hasInstantiable && hasNullableOrEmpty || hasSubstitution) return true;
}

return false;
}

function isTypeMergeableIntersectionConstituent(type: Type) {
if (
type.flags === TypeFlags.Object &&
!!((type as ObjectType).objectFlags & ObjectFlags.Anonymous) &&
!((type as ObjectType).objectFlags & ObjectFlags.Instantiated)
) {
if ((type as ObjectType).objectFlags & ObjectFlags.ReverseMapped) {
return isTypeMergeableIntersectionConstituent((type as ReverseMappedType).source);
}

return !typeHasCallOrConstructSignatures(type) && getIndexInfosOfType(type).length === 0;
}
return false;
}

Expand Down Expand Up @@ -22151,12 +22172,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// turn deferred type references into regular type references, simplify indexed access and
// conditional types, and resolve substitution types to either the substitution (on the source
// side) or the type variable (on the target side).
const source = getNormalizedType(originalSource, /*writing*/ false);
let source = getNormalizedType(originalSource, /*writing*/ false);
let target = getNormalizedType(originalTarget, /*writing*/ true);

if (source === target) return Ternary.True;

if (relation === identityRelation) {
if (source.flags & TypeFlags.Intersection) {
source = mergeIntersectionTypeIfPossible(source as IntersectionType, /*writing*/ false);
}
if (target.flags & TypeFlags.Intersection) {
target = mergeIntersectionTypeIfPossible(target as IntersectionType, /*writing*/ true);
}

if (source.flags !== target.flags) return Ternary.False;
if (source.flags & TypeFlags.Singleton) return Ternary.True;
traceUnionsOrIntersectionsTooLarge(source, target);
Expand Down Expand Up @@ -22244,6 +22272,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return Ternary.False;
}

function mergeIntersectionTypeIfPossible(type: IntersectionType, writing: boolean) {
if (every(type.types, isTypeMergeableIntersectionConstituent)) {
const reduced = getReducedType(type);
if (reduced.flags & TypeFlags.Intersection) {
type = reduced as IntersectionType;
const members = getMembersOfUnionOrIntersection(type);
const intersection = createAnonymousType(/*symbol*/ undefined, members, emptyArray, emptyArray, emptyArray);
intersection.aliasSymbol = type.aliasSymbol;
intersection.aliasTypeArguments = type.aliasTypeArguments;
return getNormalizedType(intersection, writing);
}
}
return type;
}

function reportErrorResults(originalSource: Type, originalTarget: Type, source: Type, target: Type, headMessage: DiagnosticMessage | undefined) {
const sourceHasBase = !!getSingleBaseForNonAugmentingSubtype(originalSource);
const targetHasBase = !!getSingleBaseForNonAugmentingSubtype(originalTarget);
Expand Down
70 changes: 70 additions & 0 deletions tests/baselines/reference/identityRelationIntersectionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//// [tests/cases/compiler/identityRelationIntersectionTypes.ts] ////

//// [identityRelationIntersectionTypes.ts]
namespace identityRelationIntersectionTypes {
type Equals<A, B> = (<T>() => T extends B ? 1 : 0) extends (<T>() => T extends A ? 1 : 0) ? true : false;

type GoodIntersection = Equals<{a: 1} & {b: 2}, {a: 1; b: 2}>; // true

// Interfaces aren't mergeable
interface I {i: 3};
type BadIntersection1 = Equals<{a: 1} & I, {a: 1; i: 3}>; // false

// Objects with call or constructor signatures aren't mergeable
type BadIntersection2 = Equals<{a: 1} & {b: 2; (): void}, {a: 1; b: 2; (): void}>; // false
type BadIntersection3 = Equals<{a: 1} & {b: 2; new (): void}, {a: 1; b: 2; new (): void}>; // false

// Objects with index signatures aren't mergeable
type BadIntersection4 = Equals<{a: 1} & {b: 2; [key: string]: number}, {a: 1; b: 2; [key: string]: number}>; // false

// Shouldn't merge intersection if any constituents aren't mergeable
type StillBadIntersection1 = Equals<{a: 1} & {b: 2} & I, {a: 1; b: 2; i: 3}>; // false
type StillBadIntersection2 = Equals<{a: 1} & {b: 2} & I, {a: 1; b: 2} & I>; // false

// Parentheses don't matter because intersections are flattened
type StillBadIntersection3 = Equals<({a: 1} & {b: 2}) & I, {a: 1; b: 2; i: 3}>; // false
type StillBadIntersection4 = Equals<({a: 1} & {b: 2}) & I, {a: 1; b: 2} & I>; // false

// Type aliases also don't prevent flattening
type AB = {a: 1} & {b: 2};
type StillBadIntersection5 = Equals<AB & I, {a: 1; b: 2; i: 3}>; // false
type StillBadIntersection6 = Equals<AB & I, {a: 1; b: 2} & I>; // false

type GoodDeepIntersection1 = Equals<{a: 0 | 1} & {a: 1 | 2}, {a: 1}>; // true
type GoodDeepIntersection2 = Equals<{a: {x: 1}} & {a: {y: 2}}, {a: {x: 1; y: 2}}>; // true

type GoodShallowBadDeepIntersection1 = Equals<{a: {x: 1}} & {a: {y: 2} & I}, {a: {x: 1; y: 2} & I}>; // false
type GoodShallowBadDeepIntersection2 = Equals<{a: {x: 1}} & {a: {y: 2} & I}, {a: {x: 1} & {y: 2} & I}>; // true

// Reduction applies to nested intersections
type DeepReduction = Equals<{a: {x: 1}} & {a: {x: 2}}, {a: never}>; // true

// Intersections are distributed and merged if possible with union constituents
type Distributed = Equals<
{a: 1} & {b: 2} & ({c: 3} | {d: 4} | I),
{a: 1; b: 2; c: 3} | {a: 1; b: 2; d: 4} | {a: 1} & {b: 2} & I
>; // true

// Should work with recursive types
type R1 = {a: R1; x: 1};
type R2 = {a: R2; y: 1};
type R = R1 & R2;

type Recursive1 = Equals<R, {a: R1 & R2; x: 1; y: 1}>; // true
type Recursive2 = Equals<R, {a: {a: R1 & R2; x: 1; y: 1}; x: 1; y: 1}>; // true
type Recursive3 = Equals<R, {a: {a: {a: R1 & R2; x: 1; y: 1}; x: 1; y: 1}; x: 1; y: 1}>; // true
type Recursive4 = Equals<R, {a: {a: {a: R1 & R2; x: 1; y: 0}; x: 1; y: 1}; x: 1; y: 1}>; // false
}


//// [identityRelationIntersectionTypes.js]
"use strict";
var identityRelationIntersectionTypes;
(function (identityRelationIntersectionTypes) {
;
})(identityRelationIntersectionTypes || (identityRelationIntersectionTypes = {}));


//// [identityRelationIntersectionTypes.d.ts]
declare namespace identityRelationIntersectionTypes {
}
Loading
Loading