Skip to content

Commit

Permalink
Properly handle non-generic string mapping types in unions and inters…
Browse files Browse the repository at this point in the history
…ections (#57197)
  • Loading branch information
ahejlsberg committed Jan 28, 2024
1 parent dad9f17 commit 36f9e9e
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 10 deletions.
21 changes: 14 additions & 7 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -17442,18 +17448,19 @@ 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;
const literals = filter(types, t => !!(t.flags & TypeFlags.StringLiteral));
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<string> & "foo" is just "foo"
orderedRemoveItemAt(types, i);
break;
}
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
Expand Down
97 changes: 97 additions & 0 deletions tests/baselines/reference/stringMappingReduction.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//// [tests/cases/conformance/types/literal/stringMappingReduction.ts] ////

=== stringMappingReduction.ts ===
type T00 = "prop" | `p${Lowercase<string>}p`; // `p${Lowercase<string>}p`
>T00 : Symbol(T00, Decl(stringMappingReduction.ts, 0, 0))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type T01 = "prop" | Lowercase<string>; // Lowercase<string>
>T01 : Symbol(T01, Decl(stringMappingReduction.ts, 0, 45))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type T02 = "PROP" | Lowercase<string>; // "PROP" | Lowercase<string>
>T02 : Symbol(T02, Decl(stringMappingReduction.ts, 1, 38))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type T10 = "prop" & `p${Lowercase<string>}p`; // "prop"
>T10 : Symbol(T10, Decl(stringMappingReduction.ts, 2, 38))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type T11 = "prop" & Lowercase<string>; // "prop"
>T11 : Symbol(T11, Decl(stringMappingReduction.ts, 4, 45))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type T12 = "PROP" & Lowercase<string>; // never
>T12 : Symbol(T12, Decl(stringMappingReduction.ts, 5, 38))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type T20 = "prop" | Capitalize<string>; // "prop" | Capitalize<string>
>T20 : Symbol(T20, Decl(stringMappingReduction.ts, 6, 38))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

type T21 = "Prop" | Capitalize<string>; // Capitalize<string>
>T21 : Symbol(T21, Decl(stringMappingReduction.ts, 8, 39))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

type T22 = "PROP" | Capitalize<string>; // Capitalize<string>
>T22 : Symbol(T22, Decl(stringMappingReduction.ts, 9, 39))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

type T30 = "prop" & Capitalize<string>; // never
>T30 : Symbol(T30, Decl(stringMappingReduction.ts, 10, 39))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

type T31 = "Prop" & Capitalize<string>; // "Prop"
>T31 : Symbol(T31, Decl(stringMappingReduction.ts, 12, 39))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

type T32 = "PROP" & Capitalize<string>; // "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> = 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<T extends string> = { bivarianceHack(event: EPlusFallback<Lowercase<T>>): 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<Keys>) => 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 = <T extends string>(eventQrl: VirtualEvent<T>) => {
>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))

};

72 changes: 72 additions & 0 deletions tests/baselines/reference/stringMappingReduction.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//// [tests/cases/conformance/types/literal/stringMappingReduction.ts] ////

=== stringMappingReduction.ts ===
type T00 = "prop" | `p${Lowercase<string>}p`; // `p${Lowercase<string>}p`
>T00 : `p${Lowercase<string>}p`

type T01 = "prop" | Lowercase<string>; // Lowercase<string>
>T01 : Lowercase<string>

type T02 = "PROP" | Lowercase<string>; // "PROP" | Lowercase<string>
>T02 : Lowercase<string> | "PROP"

type T10 = "prop" & `p${Lowercase<string>}p`; // "prop"
>T10 : "prop"

type T11 = "prop" & Lowercase<string>; // "prop"
>T11 : "prop"

type T12 = "PROP" & Lowercase<string>; // never
>T12 : never

type T20 = "prop" | Capitalize<string>; // "prop" | Capitalize<string>
>T20 : "prop" | Capitalize<string>

type T21 = "Prop" | Capitalize<string>; // Capitalize<string>
>T21 : Capitalize<string>

type T22 = "PROP" | Capitalize<string>; // Capitalize<string>
>T22 : Capitalize<string>

type T30 = "prop" & Capitalize<string>; // never
>T30 : never

type T31 = "Prop" & Capitalize<string>; // "Prop"
>T31 : "Prop"

type T32 = "PROP" & Capitalize<string>; // "PROP"
>T32 : "PROP"

// Repro from #57117

type EMap = { event: {} }
>EMap : { event: {}; }
>event : {}

type Keys = keyof EMap
>Keys : "event"

type EPlusFallback<C> = C extends Keys ? EMap[C] : "unrecognised event";
>EPlusFallback : EPlusFallback<C>

type VirtualEvent<T extends string> = { bivarianceHack(event: EPlusFallback<Lowercase<T>>): any; }['bivarianceHack'];
>VirtualEvent : (event: EPlusFallback<Lowercase<T>>) => any
>bivarianceHack : (event: EPlusFallback<Lowercase<T>>) => any
>event : EPlusFallback<Lowercase<T>>

declare const _virtualOn: (eventQrl: VirtualEvent<Keys>) => void;
>_virtualOn : (eventQrl: VirtualEvent<Keys>) => void
>eventQrl : (event: {}) => any

export const virtualOn = <T extends string>(eventQrl: VirtualEvent<T>) => {
>virtualOn : <T extends string>(eventQrl: VirtualEvent<T>) => void
><T extends string>(eventQrl: VirtualEvent<T>) => { _virtualOn(eventQrl);} : <T extends string>(eventQrl: VirtualEvent<T>) => void
>eventQrl : (event: EPlusFallback<Lowercase<T>>) => any

_virtualOn(eventQrl);
>_virtualOn(eventQrl) : void
>_virtualOn : (eventQrl: (event: {}) => any) => void
>eventQrl : (event: EPlusFallback<Lowercase<T>>) => any

};

2 changes: 1 addition & 1 deletion tests/baselines/reference/templateLiteralTypes3.types
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ function ft1<T extends string>(t: T, u: Uppercase<T>, u1: Uppercase<`1.${T}.3`>,
// Repro from #52685

type Boom = 'abc' | 'def' | `a${string}` | Lowercase<string>;
>Boom : `a${string}` | Lowercase<string> | "def"
>Boom : `a${string}` | Lowercase<string>

// Repro from #56582

Expand Down
29 changes: 29 additions & 0 deletions tests/cases/conformance/types/literal/stringMappingReduction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @strict: true
// @noEmit: true

type T00 = "prop" | `p${Lowercase<string>}p`; // `p${Lowercase<string>}p`
type T01 = "prop" | Lowercase<string>; // Lowercase<string>
type T02 = "PROP" | Lowercase<string>; // "PROP" | Lowercase<string>

type T10 = "prop" & `p${Lowercase<string>}p`; // "prop"
type T11 = "prop" & Lowercase<string>; // "prop"
type T12 = "PROP" & Lowercase<string>; // never

type T20 = "prop" | Capitalize<string>; // "prop" | Capitalize<string>
type T21 = "Prop" | Capitalize<string>; // Capitalize<string>
type T22 = "PROP" | Capitalize<string>; // Capitalize<string>

type T30 = "prop" & Capitalize<string>; // never
type T31 = "Prop" & Capitalize<string>; // "Prop"
type T32 = "PROP" & Capitalize<string>; // "PROP"

// Repro from #57117

type EMap = { event: {} }
type Keys = keyof EMap
type EPlusFallback<C> = C extends Keys ? EMap[C] : "unrecognised event";
type VirtualEvent<T extends string> = { bivarianceHack(event: EPlusFallback<Lowercase<T>>): any; }['bivarianceHack'];
declare const _virtualOn: (eventQrl: VirtualEvent<Keys>) => void;
export const virtualOn = <T extends string>(eventQrl: VirtualEvent<T>) => {
_virtualOn(eventQrl);
};

0 comments on commit 36f9e9e

Please sign in to comment.