Skip to content

Commit 7c9d5fa

Browse files
author
MichaelMitchell-at
committed
Merge trivially mergeable intersection types for identity comparison
1 parent 3d2b8f3 commit 7c9d5fa

5 files changed

+830
-4
lines changed

src/compiler/checker.ts

+47-4
Original file line numberDiff line numberDiff line change
@@ -14485,17 +14485,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1448514485
return getAugmentedPropertiesOfType(unionType);
1448614486
}
1448714487

14488+
const props = getMembersOfUnionOrIntersection(unionType as UnionType);
14489+
return arrayFrom(props.values());
14490+
}
14491+
14492+
function getMembersOfUnionOrIntersection(type: UnionOrIntersectionType): SymbolTable {
1448814493
const props = createSymbolTable();
14489-
for (const memberType of types) {
14494+
for (const memberType of type.types) {
1449014495
for (const { escapedName } of getAugmentedPropertiesOfType(memberType)) {
1449114496
if (!props.has(escapedName)) {
14492-
const prop = createUnionOrIntersectionProperty(unionType as UnionType, escapedName);
14497+
const prop = createUnionOrIntersectionProperty(type, escapedName);
1449314498
// May be undefined if the property is private
1449414499
if (prop) props.set(escapedName, prop);
1449514500
}
1449614501
}
1449714502
}
14498-
return arrayFrom(props.values());
14503+
return props;
1449914504
}
1450014505

1450114506
function getConstraintOfType(type: InstantiableType | UnionOrIntersectionType): Type | undefined {
@@ -21730,6 +21735,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2173021735
hasSubstitution ||= isNarrowingSubstitutionType(t); // This avoids displaying error messages with types like `T & T` when narrowing a return type
2173121736
if (hasInstantiable && hasNullableOrEmpty || hasSubstitution) return true;
2173221737
}
21738+
21739+
return false;
21740+
}
21741+
21742+
function isTypeMergeableIntersectionConstituent(type: Type) {
21743+
if (
21744+
type.flags === TypeFlags.Object &&
21745+
!!((type as ObjectType).objectFlags & ObjectFlags.Anonymous) &&
21746+
!((type as ObjectType).objectFlags & ObjectFlags.Instantiated)
21747+
) {
21748+
if ((type as ObjectType).objectFlags & ObjectFlags.ReverseMapped) {
21749+
return isTypeMergeableIntersectionConstituent((type as ReverseMappedType).source);
21750+
}
21751+
21752+
return !typeHasCallOrConstructSignatures(type) && getIndexInfosOfType(type).length === 0;
21753+
}
2173321754
return false;
2173421755
}
2173521756

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

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

2215922180
if (relation === identityRelation) {
22181+
if (source.flags & TypeFlags.Intersection) {
22182+
source = mergeIntersectionTypeIfPossible(source as IntersectionType, /*writing*/ false);
22183+
}
22184+
if (target.flags & TypeFlags.Intersection) {
22185+
target = mergeIntersectionTypeIfPossible(target as IntersectionType, /*writing*/ true);
22186+
}
22187+
2216022188
if (source.flags !== target.flags) return Ternary.False;
2216122189
if (source.flags & TypeFlags.Singleton) return Ternary.True;
2216222190
traceUnionsOrIntersectionsTooLarge(source, target);
@@ -22244,6 +22272,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2224422272
return Ternary.False;
2224522273
}
2224622274

22275+
function mergeIntersectionTypeIfPossible(type: IntersectionType, writing: boolean) {
22276+
if (every(type.types, isTypeMergeableIntersectionConstituent)) {
22277+
const reduced = getReducedType(type);
22278+
if (reduced.flags & TypeFlags.Intersection) {
22279+
type = reduced as IntersectionType;
22280+
const members = getMembersOfUnionOrIntersection(type);
22281+
const intersection = createAnonymousType(/*symbol*/ undefined, members, emptyArray, emptyArray, emptyArray);
22282+
intersection.aliasSymbol = type.aliasSymbol;
22283+
intersection.aliasTypeArguments = type.aliasTypeArguments;
22284+
return getNormalizedType(intersection, writing);
22285+
}
22286+
}
22287+
return type;
22288+
}
22289+
2224722290
function reportErrorResults(originalSource: Type, originalTarget: Type, source: Type, target: Type, headMessage: DiagnosticMessage | undefined) {
2224822291
const sourceHasBase = !!getSingleBaseForNonAugmentingSubtype(originalSource);
2224922292
const targetHasBase = !!getSingleBaseForNonAugmentingSubtype(originalTarget);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//// [tests/cases/compiler/identityRelationIntersectionTypes.ts] ////
2+
3+
//// [identityRelationIntersectionTypes.ts]
4+
namespace identityRelationIntersectionTypes {
5+
type Equals<A, B> = (<T>() => T extends B ? 1 : 0) extends (<T>() => T extends A ? 1 : 0) ? true : false;
6+
7+
type GoodIntersection = Equals<{a: 1} & {b: 2}, {a: 1; b: 2}>; // true
8+
9+
// Interfaces aren't mergeable
10+
interface I {i: 3};
11+
type BadIntersection1 = Equals<{a: 1} & I, {a: 1; i: 3}>; // false
12+
13+
// Objects with call or constructor signatures aren't mergeable
14+
type BadIntersection2 = Equals<{a: 1} & {b: 2; (): void}, {a: 1; b: 2; (): void}>; // false
15+
type BadIntersection3 = Equals<{a: 1} & {b: 2; new (): void}, {a: 1; b: 2; new (): void}>; // false
16+
17+
// Objects with index signatures aren't mergeable
18+
type BadIntersection4 = Equals<{a: 1} & {b: 2; [key: string]: number}, {a: 1; b: 2; [key: string]: number}>; // false
19+
20+
// Shouldn't merge intersection if any constituents aren't mergeable
21+
type StillBadIntersection1 = Equals<{a: 1} & {b: 2} & I, {a: 1; b: 2; i: 3}>; // false
22+
type StillBadIntersection2 = Equals<{a: 1} & {b: 2} & I, {a: 1; b: 2} & I>; // false
23+
24+
// Parentheses don't matter because intersections are flattened
25+
type StillBadIntersection3 = Equals<({a: 1} & {b: 2}) & I, {a: 1; b: 2; i: 3}>; // false
26+
type StillBadIntersection4 = Equals<({a: 1} & {b: 2}) & I, {a: 1; b: 2} & I>; // false
27+
28+
// Type aliases also don't prevent flattening
29+
type AB = {a: 1} & {b: 2};
30+
type StillBadIntersection5 = Equals<AB & I, {a: 1; b: 2; i: 3}>; // false
31+
type StillBadIntersection6 = Equals<AB & I, {a: 1; b: 2} & I>; // false
32+
33+
type GoodDeepIntersection1 = Equals<{a: 0 | 1} & {a: 1 | 2}, {a: 1}>; // true
34+
type GoodDeepIntersection2 = Equals<{a: {x: 1}} & {a: {y: 2}}, {a: {x: 1; y: 2}}>; // true
35+
36+
type GoodShallowBadDeepIntersection1 = Equals<{a: {x: 1}} & {a: {y: 2} & I}, {a: {x: 1; y: 2} & I}>; // false
37+
type GoodShallowBadDeepIntersection2 = Equals<{a: {x: 1}} & {a: {y: 2} & I}, {a: {x: 1} & {y: 2} & I}>; // true
38+
39+
// Reduction applies to nested intersections
40+
type DeepReduction = Equals<{a: {x: 1}} & {a: {x: 2}}, {a: never}>; // true
41+
42+
// Intersections are distributed and merged if possible with union constituents
43+
type Distributed = Equals<
44+
{a: 1} & {b: 2} & ({c: 3} | {d: 4} | I),
45+
{a: 1; b: 2; c: 3} | {a: 1; b: 2; d: 4} | {a: 1} & {b: 2} & I
46+
>; // true
47+
48+
// Should work with recursive types
49+
type R1 = {a: R1; x: 1};
50+
type R2 = {a: R2; y: 1};
51+
type R = R1 & R2;
52+
53+
type Recursive1 = Equals<R, {a: R1 & R2; x: 1; y: 1}>; // true
54+
type Recursive2 = Equals<R, {a: {a: R1 & R2; x: 1; y: 1}; x: 1; y: 1}>; // true
55+
type Recursive3 = Equals<R, {a: {a: {a: R1 & R2; x: 1; y: 1}; x: 1; y: 1}; x: 1; y: 1}>; // true
56+
type Recursive4 = Equals<R, {a: {a: {a: R1 & R2; x: 1; y: 0}; x: 1; y: 1}; x: 1; y: 1}>; // false
57+
}
58+
59+
60+
//// [identityRelationIntersectionTypes.js]
61+
"use strict";
62+
var identityRelationIntersectionTypes;
63+
(function (identityRelationIntersectionTypes) {
64+
;
65+
})(identityRelationIntersectionTypes || (identityRelationIntersectionTypes = {}));
66+
67+
68+
//// [identityRelationIntersectionTypes.d.ts]
69+
declare namespace identityRelationIntersectionTypes {
70+
}

0 commit comments

Comments
 (0)