Skip to content

Commit

Permalink
Merge trivially mergeable intersection types for identity comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelMitchell-at committed Dec 9, 2024
1 parent 3d2b8f3 commit f7b81cf
Show file tree
Hide file tree
Showing 5 changed files with 830 additions and 4 deletions.
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

0 comments on commit f7b81cf

Please sign in to comment.