Skip to content

Commit 36f9e9e

Browse files
authored
Properly handle non-generic string mapping types in unions and intersections (#57197)
1 parent dad9f17 commit 36f9e9e

File tree

6 files changed

+217
-10
lines changed

6 files changed

+217
-10
lines changed

src/compiler/checker.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17119,19 +17119,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1711917119
}
1712017120

1712117121
function removeStringLiteralsMatchedByTemplateLiterals(types: Type[]) {
17122-
const templates = filter(types, t => !!(t.flags & TypeFlags.TemplateLiteral) && isPatternLiteralType(t)) as TemplateLiteralType[];
17122+
const templates = filter(types, isPatternLiteralType) as (TemplateLiteralType | StringMappingType)[];
1712317123
if (templates.length) {
1712417124
let i = types.length;
1712517125
while (i > 0) {
1712617126
i--;
1712717127
const t = types[i];
17128-
if (t.flags & TypeFlags.StringLiteral && some(templates, template => isTypeMatchedByTemplateLiteralType(t, template))) {
17128+
if (t.flags & TypeFlags.StringLiteral && some(templates, template => isTypeMatchedByTemplateLiteralOrStringMapping(t, template))) {
1712917129
orderedRemoveItemAt(types, i);
1713017130
}
1713117131
}
1713217132
}
1713317133
}
1713417134

17135+
function isTypeMatchedByTemplateLiteralOrStringMapping(type: Type, template: TemplateLiteralType | StringMappingType) {
17136+
return template.flags & TypeFlags.TemplateLiteral ?
17137+
isTypeMatchedByTemplateLiteralType(type, template as TemplateLiteralType) :
17138+
isMemberOfStringMapping(type, template);
17139+
}
17140+
1713517141
function removeConstrainedTypeVariables(types: Type[]) {
1713617142
const typeVariables: TypeVariable[] = [];
1713717143
// First collect a list of the type variables occurring in constraining intersections.
@@ -17246,7 +17252,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1724617252
if (includes & (TypeFlags.Enum | TypeFlags.Literal | TypeFlags.UniqueESSymbol | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) {
1724717253
removeRedundantLiteralTypes(typeSet, includes, !!(unionReduction & UnionReduction.Subtype));
1724817254
}
17249-
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
17255+
if (includes & TypeFlags.StringLiteral && includes & (TypeFlags.TemplateLiteral | TypeFlags.StringMapping)) {
1725017256
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
1725117257
}
1725217258
if (includes & TypeFlags.IncludesConstrainedTypeVariable) {
@@ -17442,18 +17448,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1744217448
}
1744317449

1744417450
/**
17445-
* Returns `true` if the intersection of the template literals and string literals is the empty set, eg `get${string}` & "setX", and should reduce to `never`
17451+
* Returns true if the intersection of the template literals and string literals is the empty set,
17452+
* for example `get${string}` & "setX", and should reduce to never.
1744617453
*/
1744717454
function extractRedundantTemplateLiterals(types: Type[]): boolean {
1744817455
let i = types.length;
1744917456
const literals = filter(types, t => !!(t.flags & TypeFlags.StringLiteral));
1745017457
while (i > 0) {
1745117458
i--;
1745217459
const t = types[i];
17453-
if (!(t.flags & TypeFlags.TemplateLiteral)) continue;
17460+
if (!(t.flags & (TypeFlags.TemplateLiteral | TypeFlags.StringMapping))) continue;
1745417461
for (const t2 of literals) {
1745517462
if (isTypeSubtypeOf(t2, t)) {
17456-
// eg, ``get${T}` & "getX"` is just `"getX"`
17463+
// For example, `get${T}` & "getX" is just "getX", and Lowercase<string> & "foo" is just "foo"
1745717464
orderedRemoveItemAt(types, i);
1745817465
break;
1745917466
}
@@ -17563,7 +17570,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1756317570
) {
1756417571
return neverType;
1756517572
}
17566-
if (includes & TypeFlags.TemplateLiteral && includes & TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) {
17573+
if (includes & (TypeFlags.TemplateLiteral | TypeFlags.StringMapping) && includes & TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) {
1756717574
return neverType;
1756817575
}
1756917576
if (includes & TypeFlags.Any) {

src/compiler/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6125,6 +6125,8 @@ export const enum TypeFlags {
61256125
NonPrimitive = 1 << 26, // intrinsic object type
61266126
TemplateLiteral = 1 << 27, // Template literal type
61276127
StringMapping = 1 << 28, // Uppercase/Lowercase type
6128+
/** @internal */
6129+
Reserved1 = 1 << 29, // Used by union/intersection type construction
61286130

61296131
/** @internal */
61306132
AnyOrUnknown = Any | Unknown,
@@ -6172,7 +6174,7 @@ export const enum TypeFlags {
61726174
Narrowable = Any | Unknown | StructuredOrInstantiable | StringLike | NumberLike | BigIntLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive,
61736175
// The following flags are aggregated during union and intersection type construction
61746176
/** @internal */
6175-
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral,
6177+
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral | StringMapping,
61766178
// The following flags are used for different purposes during union and intersection type construction
61776179
/** @internal */
61786180
IncludesMissingType = TypeParameter,
@@ -6185,7 +6187,7 @@ export const enum TypeFlags {
61856187
/** @internal */
61866188
IncludesInstantiable = Substitution,
61876189
/** @internal */
6188-
IncludesConstrainedTypeVariable = StringMapping,
6190+
IncludesConstrainedTypeVariable = Reserved1,
61896191
/** @internal */
61906192
NotPrimitiveUnion = Any | Unknown | Void | Never | Object | Intersection | IncludesInstantiable,
61916193
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//// [tests/cases/conformance/types/literal/stringMappingReduction.ts] ////
2+
3+
=== stringMappingReduction.ts ===
4+
type T00 = "prop" | `p${Lowercase<string>}p`; // `p${Lowercase<string>}p`
5+
>T00 : Symbol(T00, Decl(stringMappingReduction.ts, 0, 0))
6+
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))
7+
8+
type T01 = "prop" | Lowercase<string>; // Lowercase<string>
9+
>T01 : Symbol(T01, Decl(stringMappingReduction.ts, 0, 45))
10+
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))
11+
12+
type T02 = "PROP" | Lowercase<string>; // "PROP" | Lowercase<string>
13+
>T02 : Symbol(T02, Decl(stringMappingReduction.ts, 1, 38))
14+
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))
15+
16+
type T10 = "prop" & `p${Lowercase<string>}p`; // "prop"
17+
>T10 : Symbol(T10, Decl(stringMappingReduction.ts, 2, 38))
18+
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))
19+
20+
type T11 = "prop" & Lowercase<string>; // "prop"
21+
>T11 : Symbol(T11, Decl(stringMappingReduction.ts, 4, 45))
22+
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))
23+
24+
type T12 = "PROP" & Lowercase<string>; // never
25+
>T12 : Symbol(T12, Decl(stringMappingReduction.ts, 5, 38))
26+
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))
27+
28+
type T20 = "prop" | Capitalize<string>; // "prop" | Capitalize<string>
29+
>T20 : Symbol(T20, Decl(stringMappingReduction.ts, 6, 38))
30+
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))
31+
32+
type T21 = "Prop" | Capitalize<string>; // Capitalize<string>
33+
>T21 : Symbol(T21, Decl(stringMappingReduction.ts, 8, 39))
34+
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))
35+
36+
type T22 = "PROP" | Capitalize<string>; // Capitalize<string>
37+
>T22 : Symbol(T22, Decl(stringMappingReduction.ts, 9, 39))
38+
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))
39+
40+
type T30 = "prop" & Capitalize<string>; // never
41+
>T30 : Symbol(T30, Decl(stringMappingReduction.ts, 10, 39))
42+
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))
43+
44+
type T31 = "Prop" & Capitalize<string>; // "Prop"
45+
>T31 : Symbol(T31, Decl(stringMappingReduction.ts, 12, 39))
46+
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))
47+
48+
type T32 = "PROP" & Capitalize<string>; // "PROP"
49+
>T32 : Symbol(T32, Decl(stringMappingReduction.ts, 13, 39))
50+
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))
51+
52+
// Repro from #57117
53+
54+
type EMap = { event: {} }
55+
>EMap : Symbol(EMap, Decl(stringMappingReduction.ts, 14, 39))
56+
>event : Symbol(event, Decl(stringMappingReduction.ts, 18, 13))
57+
58+
type Keys = keyof EMap
59+
>Keys : Symbol(Keys, Decl(stringMappingReduction.ts, 18, 25))
60+
>EMap : Symbol(EMap, Decl(stringMappingReduction.ts, 14, 39))
61+
62+
type EPlusFallback<C> = C extends Keys ? EMap[C] : "unrecognised event";
63+
>EPlusFallback : Symbol(EPlusFallback, Decl(stringMappingReduction.ts, 19, 22))
64+
>C : Symbol(C, Decl(stringMappingReduction.ts, 20, 19))
65+
>C : Symbol(C, Decl(stringMappingReduction.ts, 20, 19))
66+
>Keys : Symbol(Keys, Decl(stringMappingReduction.ts, 18, 25))
67+
>EMap : Symbol(EMap, Decl(stringMappingReduction.ts, 14, 39))
68+
>C : Symbol(C, Decl(stringMappingReduction.ts, 20, 19))
69+
70+
type VirtualEvent<T extends string> = { bivarianceHack(event: EPlusFallback<Lowercase<T>>): any; }['bivarianceHack'];
71+
>VirtualEvent : Symbol(VirtualEvent, Decl(stringMappingReduction.ts, 20, 72))
72+
>T : Symbol(T, Decl(stringMappingReduction.ts, 21, 18))
73+
>bivarianceHack : Symbol(bivarianceHack, Decl(stringMappingReduction.ts, 21, 39))
74+
>event : Symbol(event, Decl(stringMappingReduction.ts, 21, 55))
75+
>EPlusFallback : Symbol(EPlusFallback, Decl(stringMappingReduction.ts, 19, 22))
76+
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))
77+
>T : Symbol(T, Decl(stringMappingReduction.ts, 21, 18))
78+
79+
declare const _virtualOn: (eventQrl: VirtualEvent<Keys>) => void;
80+
>_virtualOn : Symbol(_virtualOn, Decl(stringMappingReduction.ts, 22, 13))
81+
>eventQrl : Symbol(eventQrl, Decl(stringMappingReduction.ts, 22, 27))
82+
>VirtualEvent : Symbol(VirtualEvent, Decl(stringMappingReduction.ts, 20, 72))
83+
>Keys : Symbol(Keys, Decl(stringMappingReduction.ts, 18, 25))
84+
85+
export const virtualOn = <T extends string>(eventQrl: VirtualEvent<T>) => {
86+
>virtualOn : Symbol(virtualOn, Decl(stringMappingReduction.ts, 23, 12))
87+
>T : Symbol(T, Decl(stringMappingReduction.ts, 23, 26))
88+
>eventQrl : Symbol(eventQrl, Decl(stringMappingReduction.ts, 23, 44))
89+
>VirtualEvent : Symbol(VirtualEvent, Decl(stringMappingReduction.ts, 20, 72))
90+
>T : Symbol(T, Decl(stringMappingReduction.ts, 23, 26))
91+
92+
_virtualOn(eventQrl);
93+
>_virtualOn : Symbol(_virtualOn, Decl(stringMappingReduction.ts, 22, 13))
94+
>eventQrl : Symbol(eventQrl, Decl(stringMappingReduction.ts, 23, 44))
95+
96+
};
97+
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//// [tests/cases/conformance/types/literal/stringMappingReduction.ts] ////
2+
3+
=== stringMappingReduction.ts ===
4+
type T00 = "prop" | `p${Lowercase<string>}p`; // `p${Lowercase<string>}p`
5+
>T00 : `p${Lowercase<string>}p`
6+
7+
type T01 = "prop" | Lowercase<string>; // Lowercase<string>
8+
>T01 : Lowercase<string>
9+
10+
type T02 = "PROP" | Lowercase<string>; // "PROP" | Lowercase<string>
11+
>T02 : Lowercase<string> | "PROP"
12+
13+
type T10 = "prop" & `p${Lowercase<string>}p`; // "prop"
14+
>T10 : "prop"
15+
16+
type T11 = "prop" & Lowercase<string>; // "prop"
17+
>T11 : "prop"
18+
19+
type T12 = "PROP" & Lowercase<string>; // never
20+
>T12 : never
21+
22+
type T20 = "prop" | Capitalize<string>; // "prop" | Capitalize<string>
23+
>T20 : "prop" | Capitalize<string>
24+
25+
type T21 = "Prop" | Capitalize<string>; // Capitalize<string>
26+
>T21 : Capitalize<string>
27+
28+
type T22 = "PROP" | Capitalize<string>; // Capitalize<string>
29+
>T22 : Capitalize<string>
30+
31+
type T30 = "prop" & Capitalize<string>; // never
32+
>T30 : never
33+
34+
type T31 = "Prop" & Capitalize<string>; // "Prop"
35+
>T31 : "Prop"
36+
37+
type T32 = "PROP" & Capitalize<string>; // "PROP"
38+
>T32 : "PROP"
39+
40+
// Repro from #57117
41+
42+
type EMap = { event: {} }
43+
>EMap : { event: {}; }
44+
>event : {}
45+
46+
type Keys = keyof EMap
47+
>Keys : "event"
48+
49+
type EPlusFallback<C> = C extends Keys ? EMap[C] : "unrecognised event";
50+
>EPlusFallback : EPlusFallback<C>
51+
52+
type VirtualEvent<T extends string> = { bivarianceHack(event: EPlusFallback<Lowercase<T>>): any; }['bivarianceHack'];
53+
>VirtualEvent : (event: EPlusFallback<Lowercase<T>>) => any
54+
>bivarianceHack : (event: EPlusFallback<Lowercase<T>>) => any
55+
>event : EPlusFallback<Lowercase<T>>
56+
57+
declare const _virtualOn: (eventQrl: VirtualEvent<Keys>) => void;
58+
>_virtualOn : (eventQrl: VirtualEvent<Keys>) => void
59+
>eventQrl : (event: {}) => any
60+
61+
export const virtualOn = <T extends string>(eventQrl: VirtualEvent<T>) => {
62+
>virtualOn : <T extends string>(eventQrl: VirtualEvent<T>) => void
63+
><T extends string>(eventQrl: VirtualEvent<T>) => { _virtualOn(eventQrl);} : <T extends string>(eventQrl: VirtualEvent<T>) => void
64+
>eventQrl : (event: EPlusFallback<Lowercase<T>>) => any
65+
66+
_virtualOn(eventQrl);
67+
>_virtualOn(eventQrl) : void
68+
>_virtualOn : (eventQrl: (event: {}) => any) => void
69+
>eventQrl : (event: EPlusFallback<Lowercase<T>>) => any
70+
71+
};
72+

tests/baselines/reference/templateLiteralTypes3.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ function ft1<T extends string>(t: T, u: Uppercase<T>, u1: Uppercase<`1.${T}.3`>,
599599
// Repro from #52685
600600

601601
type Boom = 'abc' | 'def' | `a${string}` | Lowercase<string>;
602-
>Boom : `a${string}` | Lowercase<string> | "def"
602+
>Boom : `a${string}` | Lowercase<string>
603603

604604
// Repro from #56582
605605

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// @strict: true
2+
// @noEmit: true
3+
4+
type T00 = "prop" | `p${Lowercase<string>}p`; // `p${Lowercase<string>}p`
5+
type T01 = "prop" | Lowercase<string>; // Lowercase<string>
6+
type T02 = "PROP" | Lowercase<string>; // "PROP" | Lowercase<string>
7+
8+
type T10 = "prop" & `p${Lowercase<string>}p`; // "prop"
9+
type T11 = "prop" & Lowercase<string>; // "prop"
10+
type T12 = "PROP" & Lowercase<string>; // never
11+
12+
type T20 = "prop" | Capitalize<string>; // "prop" | Capitalize<string>
13+
type T21 = "Prop" | Capitalize<string>; // Capitalize<string>
14+
type T22 = "PROP" | Capitalize<string>; // Capitalize<string>
15+
16+
type T30 = "prop" & Capitalize<string>; // never
17+
type T31 = "Prop" & Capitalize<string>; // "Prop"
18+
type T32 = "PROP" & Capitalize<string>; // "PROP"
19+
20+
// Repro from #57117
21+
22+
type EMap = { event: {} }
23+
type Keys = keyof EMap
24+
type EPlusFallback<C> = C extends Keys ? EMap[C] : "unrecognised event";
25+
type VirtualEvent<T extends string> = { bivarianceHack(event: EPlusFallback<Lowercase<T>>): any; }['bivarianceHack'];
26+
declare const _virtualOn: (eventQrl: VirtualEvent<Keys>) => void;
27+
export const virtualOn = <T extends string>(eventQrl: VirtualEvent<T>) => {
28+
_virtualOn(eventQrl);
29+
};

0 commit comments

Comments
 (0)