Skip to content

Commit fc82c67

Browse files
authored
Don't eagerly simplify reducible generic union index types (#46812)
1 parent ea4791d commit fc82c67

7 files changed

+332
-3
lines changed

src/compiler/checker.ts

+25-3
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,8 @@ namespace ts {
814814

815815
const restrictiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter(t as TypeParameter) : t);
816816
const permissiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? wildcardType : t);
817+
const uniqueLiteralType = createIntrinsicType(TypeFlags.Never, "never"); // `uniqueLiteralType` is a special `never` flagged by union reduction to behave as a literal
818+
const uniqueLiteralMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? uniqueLiteralType : t); // replace all type parameters with the unique literal type (disregarding constraints)
817819

818820
const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray);
819821
const emptyJsxObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray);
@@ -12367,10 +12369,10 @@ namespace ts {
1236712369
else if (type !== firstType) {
1236812370
checkFlags |= CheckFlags.HasNonUniformType;
1236912371
}
12370-
if (isLiteralType(type) || isPatternLiteralType(type)) {
12372+
if (isLiteralType(type) || isPatternLiteralType(type) || type === uniqueLiteralType) {
1237112373
checkFlags |= CheckFlags.HasLiteralType;
1237212374
}
12373-
if (type.flags & TypeFlags.Never) {
12375+
if (type.flags & TypeFlags.Never && type !== uniqueLiteralType) {
1237412376
checkFlags |= CheckFlags.HasNeverType;
1237512377
}
1237612378
propTypes.push(type);
@@ -15124,9 +15126,24 @@ namespace ts {
1512415126
/*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, origin);
1512515127
}
1512615128

15129+
/**
15130+
* A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations)
15131+
* must be kept generic, as that instantiation information needs to flow through the type system. By replacing all
15132+
* type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic
15133+
* to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible).
15134+
*/
15135+
function isPossiblyReducibleByInstantiation(type: UnionType): boolean {
15136+
return some(type.types, t => {
15137+
const uniqueFilled = getUniqueLiteralFilledInstantiation(t);
15138+
return getReducedType(uniqueFilled) !== uniqueFilled;
15139+
});
15140+
}
15141+
1512715142
function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type {
1512815143
type = getReducedType(type);
15129-
return type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
15144+
return type.flags & TypeFlags.Union ? isPossiblyReducibleByInstantiation(type as UnionType)
15145+
? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly)
15146+
: getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
1513015147
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
1513115148
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
1513215149
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, stringsOnly, noIndexSignatures) :
@@ -17070,6 +17087,11 @@ namespace ts {
1707017087
return type; // Nested invocation of `inferTypeForHomomorphicMappedType` or the `source` instantiated into something unmappable
1707117088
}
1707217089

17090+
function getUniqueLiteralFilledInstantiation(type: Type) {
17091+
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
17092+
type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper));
17093+
}
17094+
1707317095
function getPermissiveInstantiation(type: Type) {
1707417096
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
1707517097
type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper));

src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5244,6 +5244,8 @@ namespace ts {
52445244
/* @internal */
52455245
restrictiveInstantiation?: Type; // Instantiation with type parameters mapped to unconstrained form
52465246
/* @internal */
5247+
uniqueLiteralFilledInstantiation?: Type; // Instantiation with type parameters mapped to never type
5248+
/* @internal */
52475249
immediateBaseConstraint?: Type; // Immediate base constraint cache
52485250
/* @internal */
52495251
widened?: Type; // Cached widened form of the type
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts(33,1): error TS2741: Property 'a' is missing in type 'Gen2<ABC.B>' but required in type 'Gen2<ABC.A>'.
2+
tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts(34,1): error TS2741: Property 'b' is missing in type 'Gen2<ABC.A>' but required in type 'Gen2<ABC.B>'.
3+
4+
5+
==== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts (2 errors) ====
6+
enum ABC { A, B }
7+
8+
type Gen<T extends ABC> = { v: T; } & (
9+
{
10+
v: ABC.A,
11+
a: string,
12+
} | {
13+
v: ABC.B,
14+
b: string,
15+
}
16+
)
17+
18+
// Quick info: ???
19+
//
20+
// type Gen2<T extends ABC> = {
21+
// v: string;
22+
// }
23+
//
24+
type Gen2<T extends ABC> = {
25+
[Property in keyof Gen<T>]: string;
26+
};
27+
28+
// 'a' and 'b' properties required !?!?
29+
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
30+
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };
31+
32+
// 'v' ???
33+
type K = keyof Gen2<ABC.A>;
34+
35+
// :(
36+
declare let a: Gen2<ABC.A>;
37+
declare let b: Gen2<ABC.B>;
38+
a = b;
39+
~
40+
!!! error TS2741: Property 'a' is missing in type 'Gen2<ABC.B>' but required in type 'Gen2<ABC.A>'.
41+
!!! related TS2728 tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts:6:5: 'a' is declared here.
42+
b = a;
43+
~
44+
!!! error TS2741: Property 'b' is missing in type 'Gen2<ABC.A>' but required in type 'Gen2<ABC.B>'.
45+
!!! related TS2728 tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts:9:5: 'b' is declared here.
46+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//// [mappedTypeNotMistakenlyHomomorphic.ts]
2+
enum ABC { A, B }
3+
4+
type Gen<T extends ABC> = { v: T; } & (
5+
{
6+
v: ABC.A,
7+
a: string,
8+
} | {
9+
v: ABC.B,
10+
b: string,
11+
}
12+
)
13+
14+
// Quick info: ???
15+
//
16+
// type Gen2<T extends ABC> = {
17+
// v: string;
18+
// }
19+
//
20+
type Gen2<T extends ABC> = {
21+
[Property in keyof Gen<T>]: string;
22+
};
23+
24+
// 'a' and 'b' properties required !?!?
25+
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
26+
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };
27+
28+
// 'v' ???
29+
type K = keyof Gen2<ABC.A>;
30+
31+
// :(
32+
declare let a: Gen2<ABC.A>;
33+
declare let b: Gen2<ABC.B>;
34+
a = b;
35+
b = a;
36+
37+
38+
//// [mappedTypeNotMistakenlyHomomorphic.js]
39+
var ABC;
40+
(function (ABC) {
41+
ABC[ABC["A"] = 0] = "A";
42+
ABC[ABC["B"] = 1] = "B";
43+
})(ABC || (ABC = {}));
44+
// 'a' and 'b' properties required !?!?
45+
var gen2TypeA = { v: "I am A", a: "" };
46+
var gen2TypeB = { v: "I am B", b: "" };
47+
a = b;
48+
b = a;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
=== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts ===
2+
enum ABC { A, B }
3+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
4+
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))
5+
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))
6+
7+
type Gen<T extends ABC> = { v: T; } & (
8+
>Gen : Symbol(Gen, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 17))
9+
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 2, 9))
10+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
11+
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 2, 27))
12+
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 2, 9))
13+
{
14+
v: ABC.A,
15+
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 3, 3))
16+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
17+
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))
18+
19+
a: string,
20+
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 4, 13))
21+
22+
} | {
23+
v: ABC.B,
24+
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 6, 7))
25+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
26+
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))
27+
28+
b: string,
29+
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 7, 13))
30+
}
31+
)
32+
33+
// Quick info: ???
34+
//
35+
// type Gen2<T extends ABC> = {
36+
// v: string;
37+
// }
38+
//
39+
type Gen2<T extends ABC> = {
40+
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
41+
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 18, 10))
42+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
43+
44+
[Property in keyof Gen<T>]: string;
45+
>Property : Symbol(Property, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 19, 3))
46+
>Gen : Symbol(Gen, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 17))
47+
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 18, 10))
48+
49+
};
50+
51+
// 'a' and 'b' properties required !?!?
52+
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
53+
>gen2TypeA : Symbol(gen2TypeA, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 23, 5))
54+
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
55+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
56+
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))
57+
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 23, 32))
58+
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 23, 46))
59+
60+
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };
61+
>gen2TypeB : Symbol(gen2TypeB, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 5))
62+
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
63+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
64+
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))
65+
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 32))
66+
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 46))
67+
68+
// 'v' ???
69+
type K = keyof Gen2<ABC.A>;
70+
>K : Symbol(K, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 55))
71+
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
72+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
73+
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))
74+
75+
// :(
76+
declare let a: Gen2<ABC.A>;
77+
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 30, 11))
78+
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
79+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
80+
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))
81+
82+
declare let b: Gen2<ABC.B>;
83+
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 31, 11))
84+
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
85+
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
86+
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))
87+
88+
a = b;
89+
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 30, 11))
90+
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 31, 11))
91+
92+
b = a;
93+
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 31, 11))
94+
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 30, 11))
95+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
=== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts ===
2+
enum ABC { A, B }
3+
>ABC : ABC
4+
>A : ABC.A
5+
>B : ABC.B
6+
7+
type Gen<T extends ABC> = { v: T; } & (
8+
>Gen : Gen<T>
9+
>v : T
10+
{
11+
v: ABC.A,
12+
>v : ABC.A
13+
>ABC : any
14+
15+
a: string,
16+
>a : string
17+
18+
} | {
19+
v: ABC.B,
20+
>v : ABC.B
21+
>ABC : any
22+
23+
b: string,
24+
>b : string
25+
}
26+
)
27+
28+
// Quick info: ???
29+
//
30+
// type Gen2<T extends ABC> = {
31+
// v: string;
32+
// }
33+
//
34+
type Gen2<T extends ABC> = {
35+
>Gen2 : Gen2<T>
36+
37+
[Property in keyof Gen<T>]: string;
38+
};
39+
40+
// 'a' and 'b' properties required !?!?
41+
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
42+
>gen2TypeA : Gen2<ABC.A>
43+
>ABC : any
44+
>{ v: "I am A", a: "" } : { v: string; a: string; }
45+
>v : string
46+
>"I am A" : "I am A"
47+
>a : string
48+
>"" : ""
49+
50+
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };
51+
>gen2TypeB : Gen2<ABC.B>
52+
>ABC : any
53+
>{ v: "I am B", b: "" } : { v: string; b: string; }
54+
>v : string
55+
>"I am B" : "I am B"
56+
>b : string
57+
>"" : ""
58+
59+
// 'v' ???
60+
type K = keyof Gen2<ABC.A>;
61+
>K : "v" | "a"
62+
>ABC : any
63+
64+
// :(
65+
declare let a: Gen2<ABC.A>;
66+
>a : Gen2<ABC.A>
67+
>ABC : any
68+
69+
declare let b: Gen2<ABC.B>;
70+
>b : Gen2<ABC.B>
71+
>ABC : any
72+
73+
a = b;
74+
>a = b : Gen2<ABC.B>
75+
>a : Gen2<ABC.A>
76+
>b : Gen2<ABC.B>
77+
78+
b = a;
79+
>b = a : Gen2<ABC.A>
80+
>b : Gen2<ABC.B>
81+
>a : Gen2<ABC.A>
82+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
enum ABC { A, B }
2+
3+
type Gen<T extends ABC> = { v: T; } & (
4+
{
5+
v: ABC.A,
6+
a: string,
7+
} | {
8+
v: ABC.B,
9+
b: string,
10+
}
11+
)
12+
13+
// Quick info: ???
14+
//
15+
// type Gen2<T extends ABC> = {
16+
// v: string;
17+
// }
18+
//
19+
type Gen2<T extends ABC> = {
20+
[Property in keyof Gen<T>]: string;
21+
};
22+
23+
// 'a' and 'b' properties required !?!?
24+
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
25+
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };
26+
27+
// 'v' ???
28+
type K = keyof Gen2<ABC.A>;
29+
30+
// :(
31+
declare let a: Gen2<ABC.A>;
32+
declare let b: Gen2<ABC.B>;
33+
a = b;
34+
b = a;

0 commit comments

Comments
 (0)