From 84f2a5e7367cc7cb37d744696c34c65af1a303f0 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 26 Jan 2024 16:35:23 -0800 Subject: [PATCH 1/3] Properly handle non-generic string mapping types in unions and intersections --- src/compiler/checker.ts | 21 ++++++++++++++------- src/compiler/types.ts | 6 ++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3b3d4256fecfc..66398197916e3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17119,19 +17119,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function removeStringLiteralsMatchedByTemplateLiterals(types: Type[]) { - const templates = filter(types, t => !!(t.flags & TypeFlags.TemplateLiteral) && isPatternLiteralType(t)) as TemplateLiteralType[]; + const templates = filter(types, isPatternLiteralType) as (TemplateLiteralType | StringMappingType)[]; if (templates.length) { let i = types.length; while (i > 0) { i--; const t = types[i]; - if (t.flags & TypeFlags.StringLiteral && some(templates, template => isTypeMatchedByTemplateLiteralType(t, template))) { + if (t.flags & TypeFlags.StringLiteral && some(templates, template => isTypeMatchedByTemplateLiteralOrStringMapping(t, template))) { orderedRemoveItemAt(types, i); } } } } + function isTypeMatchedByTemplateLiteralOrStringMapping(type: Type, template: TemplateLiteralType | StringMappingType) { + return template.flags & TypeFlags.TemplateLiteral ? + isTypeMatchedByTemplateLiteralType(type, template as TemplateLiteralType) : + isMemberOfStringMapping(type, template); + } + function removeConstrainedTypeVariables(types: Type[]) { const typeVariables: TypeVariable[] = []; // First collect a list of the type variables occurring in constraining intersections. @@ -17246,7 +17252,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (includes & (TypeFlags.Enum | TypeFlags.Literal | TypeFlags.UniqueESSymbol | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) { removeRedundantLiteralTypes(typeSet, includes, !!(unionReduction & UnionReduction.Subtype)); } - if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) { + if (includes & TypeFlags.StringLiteral && includes & (TypeFlags.TemplateLiteral | TypeFlags.StringMapping)) { removeStringLiteralsMatchedByTemplateLiterals(typeSet); } if (includes & TypeFlags.IncludesConstrainedTypeVariable) { @@ -17442,7 +17448,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } /** - * 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` + * Returns true if the intersection of the template literals and string literals is the empty set, + * for example `get${string}` & "setX", and should reduce to never. */ function extractRedundantTemplateLiterals(types: Type[]): boolean { let i = types.length; @@ -17450,10 +17457,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { while (i > 0) { i--; const t = types[i]; - if (!(t.flags & TypeFlags.TemplateLiteral)) continue; + if (!(t.flags & (TypeFlags.TemplateLiteral | TypeFlags.StringMapping))) continue; for (const t2 of literals) { if (isTypeSubtypeOf(t2, t)) { - // eg, ``get${T}` & "getX"` is just `"getX"` + // For example, `get${T}` & "getX" is just "getX", and Lowercase & "foo" is just "foo" orderedRemoveItemAt(types, i); break; } @@ -17563,7 +17570,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { ) { return neverType; } - if (includes & TypeFlags.TemplateLiteral && includes & TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) { + if (includes & (TypeFlags.TemplateLiteral | TypeFlags.StringMapping) && includes & TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) { return neverType; } if (includes & TypeFlags.Any) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 48d8e2d01bb94..28061c88e94bb 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6125,6 +6125,8 @@ export const enum TypeFlags { NonPrimitive = 1 << 26, // intrinsic object type TemplateLiteral = 1 << 27, // Template literal type StringMapping = 1 << 28, // Uppercase/Lowercase type + /** @internal */ + Reserved1 = 1 << 29, // Used by union/intersection type construction /** @internal */ AnyOrUnknown = Any | Unknown, @@ -6172,7 +6174,7 @@ export const enum TypeFlags { Narrowable = Any | Unknown | StructuredOrInstantiable | StringLike | NumberLike | BigIntLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive, // The following flags are aggregated during union and intersection type construction /** @internal */ - IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral, + IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral | StringMapping, // The following flags are used for different purposes during union and intersection type construction /** @internal */ IncludesMissingType = TypeParameter, @@ -6185,7 +6187,7 @@ export const enum TypeFlags { /** @internal */ IncludesInstantiable = Substitution, /** @internal */ - IncludesConstrainedTypeVariable = StringMapping, + IncludesConstrainedTypeVariable = Reserved1, /** @internal */ NotPrimitiveUnion = Any | Unknown | Void | Never | Object | Intersection | IncludesInstantiable, } From cf6b384b37e7d04920787372ce5e9d3ed046b71c Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 26 Jan 2024 16:35:47 -0800 Subject: [PATCH 2/3] Accept new baselines --- tests/baselines/reference/templateLiteralTypes3.types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/baselines/reference/templateLiteralTypes3.types b/tests/baselines/reference/templateLiteralTypes3.types index a37862c886cf6..076b533f179a9 100644 --- a/tests/baselines/reference/templateLiteralTypes3.types +++ b/tests/baselines/reference/templateLiteralTypes3.types @@ -599,7 +599,7 @@ function ft1(t: T, u: Uppercase, u1: Uppercase<`1.${T}.3`>, // Repro from #52685 type Boom = 'abc' | 'def' | `a${string}` | Lowercase; ->Boom : `a${string}` | Lowercase | "def" +>Boom : `a${string}` | Lowercase // Repro from #56582 From f445b4fd096aec5b13cf164a3c8d5a982e011dba Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 26 Jan 2024 17:19:03 -0800 Subject: [PATCH 3/3] Add tests --- .../reference/stringMappingReduction.symbols | 97 +++++++++++++++++++ .../reference/stringMappingReduction.types | 72 ++++++++++++++ .../types/literal/stringMappingReduction.ts | 29 ++++++ 3 files changed, 198 insertions(+) create mode 100644 tests/baselines/reference/stringMappingReduction.symbols create mode 100644 tests/baselines/reference/stringMappingReduction.types create mode 100644 tests/cases/conformance/types/literal/stringMappingReduction.ts diff --git a/tests/baselines/reference/stringMappingReduction.symbols b/tests/baselines/reference/stringMappingReduction.symbols new file mode 100644 index 0000000000000..6dc5ade123c6a --- /dev/null +++ b/tests/baselines/reference/stringMappingReduction.symbols @@ -0,0 +1,97 @@ +//// [tests/cases/conformance/types/literal/stringMappingReduction.ts] //// + +=== stringMappingReduction.ts === +type T00 = "prop" | `p${Lowercase}p`; // `p${Lowercase}p` +>T00 : Symbol(T00, Decl(stringMappingReduction.ts, 0, 0)) +>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --)) + +type T01 = "prop" | Lowercase; // Lowercase +>T01 : Symbol(T01, Decl(stringMappingReduction.ts, 0, 45)) +>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --)) + +type T02 = "PROP" | Lowercase; // "PROP" | Lowercase +>T02 : Symbol(T02, Decl(stringMappingReduction.ts, 1, 38)) +>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --)) + +type T10 = "prop" & `p${Lowercase}p`; // "prop" +>T10 : Symbol(T10, Decl(stringMappingReduction.ts, 2, 38)) +>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --)) + +type T11 = "prop" & Lowercase; // "prop" +>T11 : Symbol(T11, Decl(stringMappingReduction.ts, 4, 45)) +>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --)) + +type T12 = "PROP" & Lowercase; // never +>T12 : Symbol(T12, Decl(stringMappingReduction.ts, 5, 38)) +>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --)) + +type T20 = "prop" | Capitalize; // "prop" | Capitalize +>T20 : Symbol(T20, Decl(stringMappingReduction.ts, 6, 38)) +>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --)) + +type T21 = "Prop" | Capitalize; // Capitalize +>T21 : Symbol(T21, Decl(stringMappingReduction.ts, 8, 39)) +>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --)) + +type T22 = "PROP" | Capitalize; // Capitalize +>T22 : Symbol(T22, Decl(stringMappingReduction.ts, 9, 39)) +>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --)) + +type T30 = "prop" & Capitalize; // never +>T30 : Symbol(T30, Decl(stringMappingReduction.ts, 10, 39)) +>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --)) + +type T31 = "Prop" & Capitalize; // "Prop" +>T31 : Symbol(T31, Decl(stringMappingReduction.ts, 12, 39)) +>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --)) + +type T32 = "PROP" & Capitalize; // "PROP" +>T32 : Symbol(T32, Decl(stringMappingReduction.ts, 13, 39)) +>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --)) + +// Repro from #57117 + +type EMap = { event: {} } +>EMap : Symbol(EMap, Decl(stringMappingReduction.ts, 14, 39)) +>event : Symbol(event, Decl(stringMappingReduction.ts, 18, 13)) + +type Keys = keyof EMap +>Keys : Symbol(Keys, Decl(stringMappingReduction.ts, 18, 25)) +>EMap : Symbol(EMap, Decl(stringMappingReduction.ts, 14, 39)) + +type EPlusFallback = C extends Keys ? EMap[C] : "unrecognised event"; +>EPlusFallback : Symbol(EPlusFallback, Decl(stringMappingReduction.ts, 19, 22)) +>C : Symbol(C, Decl(stringMappingReduction.ts, 20, 19)) +>C : Symbol(C, Decl(stringMappingReduction.ts, 20, 19)) +>Keys : Symbol(Keys, Decl(stringMappingReduction.ts, 18, 25)) +>EMap : Symbol(EMap, Decl(stringMappingReduction.ts, 14, 39)) +>C : Symbol(C, Decl(stringMappingReduction.ts, 20, 19)) + +type VirtualEvent = { bivarianceHack(event: EPlusFallback>): any; }['bivarianceHack']; +>VirtualEvent : Symbol(VirtualEvent, Decl(stringMappingReduction.ts, 20, 72)) +>T : Symbol(T, Decl(stringMappingReduction.ts, 21, 18)) +>bivarianceHack : Symbol(bivarianceHack, Decl(stringMappingReduction.ts, 21, 39)) +>event : Symbol(event, Decl(stringMappingReduction.ts, 21, 55)) +>EPlusFallback : Symbol(EPlusFallback, Decl(stringMappingReduction.ts, 19, 22)) +>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(stringMappingReduction.ts, 21, 18)) + +declare const _virtualOn: (eventQrl: VirtualEvent) => void; +>_virtualOn : Symbol(_virtualOn, Decl(stringMappingReduction.ts, 22, 13)) +>eventQrl : Symbol(eventQrl, Decl(stringMappingReduction.ts, 22, 27)) +>VirtualEvent : Symbol(VirtualEvent, Decl(stringMappingReduction.ts, 20, 72)) +>Keys : Symbol(Keys, Decl(stringMappingReduction.ts, 18, 25)) + +export const virtualOn = (eventQrl: VirtualEvent) => { +>virtualOn : Symbol(virtualOn, Decl(stringMappingReduction.ts, 23, 12)) +>T : Symbol(T, Decl(stringMappingReduction.ts, 23, 26)) +>eventQrl : Symbol(eventQrl, Decl(stringMappingReduction.ts, 23, 44)) +>VirtualEvent : Symbol(VirtualEvent, Decl(stringMappingReduction.ts, 20, 72)) +>T : Symbol(T, Decl(stringMappingReduction.ts, 23, 26)) + + _virtualOn(eventQrl); +>_virtualOn : Symbol(_virtualOn, Decl(stringMappingReduction.ts, 22, 13)) +>eventQrl : Symbol(eventQrl, Decl(stringMappingReduction.ts, 23, 44)) + +}; + diff --git a/tests/baselines/reference/stringMappingReduction.types b/tests/baselines/reference/stringMappingReduction.types new file mode 100644 index 0000000000000..117e928cd1e22 --- /dev/null +++ b/tests/baselines/reference/stringMappingReduction.types @@ -0,0 +1,72 @@ +//// [tests/cases/conformance/types/literal/stringMappingReduction.ts] //// + +=== stringMappingReduction.ts === +type T00 = "prop" | `p${Lowercase}p`; // `p${Lowercase}p` +>T00 : `p${Lowercase}p` + +type T01 = "prop" | Lowercase; // Lowercase +>T01 : Lowercase + +type T02 = "PROP" | Lowercase; // "PROP" | Lowercase +>T02 : Lowercase | "PROP" + +type T10 = "prop" & `p${Lowercase}p`; // "prop" +>T10 : "prop" + +type T11 = "prop" & Lowercase; // "prop" +>T11 : "prop" + +type T12 = "PROP" & Lowercase; // never +>T12 : never + +type T20 = "prop" | Capitalize; // "prop" | Capitalize +>T20 : "prop" | Capitalize + +type T21 = "Prop" | Capitalize; // Capitalize +>T21 : Capitalize + +type T22 = "PROP" | Capitalize; // Capitalize +>T22 : Capitalize + +type T30 = "prop" & Capitalize; // never +>T30 : never + +type T31 = "Prop" & Capitalize; // "Prop" +>T31 : "Prop" + +type T32 = "PROP" & Capitalize; // "PROP" +>T32 : "PROP" + +// Repro from #57117 + +type EMap = { event: {} } +>EMap : { event: {}; } +>event : {} + +type Keys = keyof EMap +>Keys : "event" + +type EPlusFallback = C extends Keys ? EMap[C] : "unrecognised event"; +>EPlusFallback : EPlusFallback + +type VirtualEvent = { bivarianceHack(event: EPlusFallback>): any; }['bivarianceHack']; +>VirtualEvent : (event: EPlusFallback>) => any +>bivarianceHack : (event: EPlusFallback>) => any +>event : EPlusFallback> + +declare const _virtualOn: (eventQrl: VirtualEvent) => void; +>_virtualOn : (eventQrl: VirtualEvent) => void +>eventQrl : (event: {}) => any + +export const virtualOn = (eventQrl: VirtualEvent) => { +>virtualOn : (eventQrl: VirtualEvent) => void +>(eventQrl: VirtualEvent) => { _virtualOn(eventQrl);} : (eventQrl: VirtualEvent) => void +>eventQrl : (event: EPlusFallback>) => any + + _virtualOn(eventQrl); +>_virtualOn(eventQrl) : void +>_virtualOn : (eventQrl: (event: {}) => any) => void +>eventQrl : (event: EPlusFallback>) => any + +}; + diff --git a/tests/cases/conformance/types/literal/stringMappingReduction.ts b/tests/cases/conformance/types/literal/stringMappingReduction.ts new file mode 100644 index 0000000000000..6faec14480e84 --- /dev/null +++ b/tests/cases/conformance/types/literal/stringMappingReduction.ts @@ -0,0 +1,29 @@ +// @strict: true +// @noEmit: true + +type T00 = "prop" | `p${Lowercase}p`; // `p${Lowercase}p` +type T01 = "prop" | Lowercase; // Lowercase +type T02 = "PROP" | Lowercase; // "PROP" | Lowercase + +type T10 = "prop" & `p${Lowercase}p`; // "prop" +type T11 = "prop" & Lowercase; // "prop" +type T12 = "PROP" & Lowercase; // never + +type T20 = "prop" | Capitalize; // "prop" | Capitalize +type T21 = "Prop" | Capitalize; // Capitalize +type T22 = "PROP" | Capitalize; // Capitalize + +type T30 = "prop" & Capitalize; // never +type T31 = "Prop" & Capitalize; // "Prop" +type T32 = "PROP" & Capitalize; // "PROP" + +// Repro from #57117 + +type EMap = { event: {} } +type Keys = keyof EMap +type EPlusFallback = C extends Keys ? EMap[C] : "unrecognised event"; +type VirtualEvent = { bivarianceHack(event: EPlusFallback>): any; }['bivarianceHack']; +declare const _virtualOn: (eventQrl: VirtualEvent) => void; +export const virtualOn = (eventQrl: VirtualEvent) => { + _virtualOn(eventQrl); +};