Skip to content

Commit c42b8f7

Browse files
committed
New deduplication algorithm for union types
1 parent c7b0732 commit c42b8f7

File tree

1 file changed

+80
-24
lines changed

1 file changed

+80
-24
lines changed

src/compiler/checker.ts

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3975,26 +3975,59 @@ namespace ts {
39753975
}
39763976
}
39773977

3978-
function isSubtypeOfAny(candidate: Type, types: Type[]): boolean {
3978+
function isObjectLiteralTypeDuplicateOf(source: ObjectType, target: ObjectType): boolean {
3979+
let sourceProperties = getPropertiesOfObjectType(source);
3980+
let targetProperties = getPropertiesOfObjectType(target);
3981+
if (sourceProperties.length !== targetProperties.length) {
3982+
return false;
3983+
}
3984+
for (let sourceProp of sourceProperties) {
3985+
let targetProp = getPropertyOfObjectType(target, sourceProp.name);
3986+
if (!targetProp ||
3987+
getDeclarationFlagsFromSymbol(targetProp) & (NodeFlags.Private | NodeFlags.Protected) ||
3988+
!isTypeDuplicateOf(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp))) {
3989+
return false;
3990+
}
3991+
}
3992+
return true;
3993+
}
3994+
3995+
function isTypeDuplicateOf(source: Type, target: Type): boolean {
3996+
if (source === target) {
3997+
return true;
3998+
}
3999+
if (source.flags & TypeFlags.Undefined || source.flags & TypeFlags.Null && !(target.flags & TypeFlags.Undefined)) {
4000+
return true;
4001+
}
4002+
if (source.flags & TypeFlags.ObjectLiteral && target.flags & TypeFlags.ObjectType) {
4003+
return isObjectLiteralTypeDuplicateOf(<ObjectType>source, <ObjectType>target);
4004+
}
4005+
if (isArrayType(source) && isArrayType(target)) {
4006+
return isTypeDuplicateOf((<TypeReference>source).typeArguments[0], (<TypeReference>target).typeArguments[0]);
4007+
}
4008+
return isTypeIdenticalTo(source, target);
4009+
}
4010+
4011+
function isTypeDuplicateOfSomeType(candidate: Type, types: Type[]): boolean {
39794012
for (let type of types) {
3980-
if (candidate !== type && isTypeSubtypeOf(getRegularTypeOfObjectLiteral(candidate), type)) {
4013+
if (candidate !== type && isTypeDuplicateOf(candidate, type)) {
39814014
return true;
39824015
}
39834016
}
39844017
return false;
39854018
}
39864019

3987-
function removeSubtypes(types: Type[]) {
4020+
function removeDuplicateTypes(types: Type[]) {
39884021
let i = types.length;
39894022
while (i > 0) {
39904023
i--;
3991-
if (isSubtypeOfAny(types[i], types)) {
4024+
if (isTypeDuplicateOfSomeType(types[i], types)) {
39924025
types.splice(i, 1);
39934026
}
39944027
}
39954028
}
39964029

3997-
function containsTypeAny(types: Type[]) {
4030+
function containsTypeAny(types: Type[]): boolean {
39984031
for (let type of types) {
39994032
if (isTypeAny(type)) {
40004033
return true;
@@ -4017,27 +4050,28 @@ namespace ts {
40174050
return type1.id - type2.id;
40184051
}
40194052

4020-
// The noSubtypeReduction flag is there because it isn't possible to always do subtype reduction. The flag
4021-
// is true when creating a union type from a type node and when instantiating a union type. In both of those
4022-
// cases subtype reduction has to be deferred to properly support recursive union types. For example, a
4023-
// type alias of the form "type Item = string | (() => Item)" cannot be reduced during its declaration.
4024-
function getUnionType(types: Type[], noSubtypeReduction?: boolean): Type {
4053+
// The noDeduplication flag exists because it isn't always possible to deduplicate the constituent types.
4054+
// The flag is true when creating a union type from a type node and when instantiating a union type. In
4055+
// both of those cases subtype deduplication has to be deferred to properly support recursive union types.
4056+
// For example, a type alias of the form "type Item = string | (() => Item)" cannot be deduplicated during
4057+
// its declaration.
4058+
function getUnionType(types: Type[], noDeduplication?: boolean): Type {
40254059
if (types.length === 0) {
40264060
return emptyObjectType;
40274061
}
40284062
let typeSet: Type[] = [];
40294063
addTypesToSet(typeSet, types, TypeFlags.Union);
4030-
typeSet.sort(compareTypeIds);
4031-
if (noSubtypeReduction) {
4032-
if (containsTypeAny(typeSet)) {
4033-
return anyType;
4034-
}
4064+
if (containsTypeAny(typeSet)) {
4065+
return anyType;
4066+
}
4067+
if (noDeduplication) {
40354068
removeAllButLast(typeSet, undefinedType);
40364069
removeAllButLast(typeSet, nullType);
40374070
}
40384071
else {
4039-
removeSubtypes(typeSet);
4072+
removeDuplicateTypes(typeSet);
40404073
}
4074+
typeSet.sort(compareTypeIds);
40414075
if (typeSet.length === 1) {
40424076
return typeSet[0];
40434077
}
@@ -4046,19 +4080,41 @@ namespace ts {
40464080
if (!type) {
40474081
type = unionTypes[id] = <UnionType>createObjectType(TypeFlags.Union | getWideningFlagsOfTypes(typeSet));
40484082
type.types = typeSet;
4049-
type.reducedType = noSubtypeReduction ? undefined : type;
40504083
}
40514084
return type;
40524085
}
40534086

4054-
// Subtype reduction is basically an optimization we do to avoid excessively large union types, which take longer
4055-
// to process and look strange in quick info and error messages. Semantically there is no difference between the
4056-
// reduced type and the type itself. So, when we detect a circularity we simply say that the reduced type is the
4057-
// type itself.
4087+
function isTypeSubtypeOfSomeType(candidate: Type, types: Type[]): boolean {
4088+
for (let type of types) {
4089+
if (candidate !== type && isTypeSubtypeOf(candidate, type)) {
4090+
return true;
4091+
}
4092+
}
4093+
return false;
4094+
}
4095+
4096+
function removeSubtypes(types: Type[]): Type[] {
4097+
let result = types;
4098+
let i = result.length;
4099+
while (i > 0) {
4100+
i--;
4101+
if (isTypeSubtypeOfSomeType(result[i], result)) {
4102+
if (result === types) {
4103+
result = types.slice(0);
4104+
}
4105+
result.splice(i, 1);
4106+
}
4107+
}
4108+
return result;
4109+
}
4110+
4111+
// The reduced type is a union type in which no constituent type is a subtype of another
4112+
// constituent type.
40584113
function getReducedTypeOfUnionType(type: UnionType): Type {
40594114
if (!type.reducedType) {
40604115
type.reducedType = circularType;
4061-
let reducedType = getUnionType(type.types, /*noSubtypeReduction*/ false);
4116+
let typesWithoutSubtypes = removeSubtypes(type.types);
4117+
let reducedType = typesWithoutSubtypes === type.types ? type : getUnionType(typesWithoutSubtypes);
40624118
if (type.reducedType === circularType) {
40634119
type.reducedType = reducedType;
40644120
}
@@ -4072,7 +4128,7 @@ namespace ts {
40724128
function getTypeFromUnionTypeNode(node: UnionTypeNode): Type {
40734129
let links = getNodeLinks(node);
40744130
if (!links.resolvedType) {
4075-
links.resolvedType = getUnionType(map(node.types, getTypeFromTypeNode), /*noSubtypeReduction*/ true);
4131+
links.resolvedType = getUnionType(map(node.types, getTypeFromTypeNode), /*noDeduplication*/ true);
40764132
}
40774133
return links.resolvedType;
40784134
}
@@ -4355,7 +4411,7 @@ namespace ts {
43554411
return createTupleType(instantiateList((<TupleType>type).elementTypes, mapper, instantiateType));
43564412
}
43574413
if (type.flags & TypeFlags.Union) {
4358-
return getUnionType(instantiateList((<UnionType>type).types, mapper, instantiateType), /*noSubtypeReduction*/ true);
4414+
return getUnionType(instantiateList((<UnionType>type).types, mapper, instantiateType), /*noDeduplication*/ true);
43594415
}
43604416
if (type.flags & TypeFlags.Intersection) {
43614417
return getIntersectionType(instantiateList((<IntersectionType>type).types, mapper, instantiateType));

0 commit comments

Comments
 (0)