Skip to content

Properly handle non-generic string mapping types in unions and intersections #57197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of funky - I'd just directly make this IncludesConstrainedTypeVariable.


/** @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);
};