Skip to content

Allow nongeneric string mapping types to exist #47050

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 9 commits into from
Jun 17, 2022
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
64 changes: 57 additions & 7 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12182,7 +12182,7 @@ namespace ts {
}
if (t.flags & TypeFlags.StringMapping) {
const constraint = getBaseConstraint((t as StringMappingType).type);
return constraint ? getStringMappingType((t as StringMappingType).symbol, constraint) : stringType;
return constraint && constraint !== (t as StringMappingType).type ? getStringMappingType((t as StringMappingType).symbol, constraint) : stringType;
}
if (t.flags & TypeFlags.IndexedAccess) {
if (isMappedTypeGenericIndexedAccess(t)) {
Expand Down Expand Up @@ -15381,8 +15381,11 @@ namespace ts {

function getStringMappingType(symbol: Symbol, type: Type): Type {
return type.flags & (TypeFlags.Union | TypeFlags.Never) ? mapType(type, t => getStringMappingType(symbol, t)) :
isGenericIndexType(type) ? getStringMappingTypeForGenericType(symbol, type) :
// Mapping<Mapping<T>> === Mapping<T>
type.flags & TypeFlags.StringMapping && symbol === type.symbol ? type :
isGenericIndexType(type) || isPatternLiteralPlaceholderType(type) ? getStringMappingTypeForGenericType(symbol, isPatternLiteralPlaceholderType(type) && !(type.flags & TypeFlags.StringMapping) ? getTemplateLiteralType(["", ""], [type]) : type) :
type.flags & TypeFlags.StringLiteral ? getStringLiteralType(applyStringMapping(symbol, (type as StringLiteralType).value)) :
type.flags & TypeFlags.TemplateLiteral ? getTemplateLiteralType(...applyTemplateStringMapping(symbol, (type as TemplateLiteralType).texts, (type as TemplateLiteralType).types)) :
type;
}

Expand All @@ -15396,6 +15399,16 @@ namespace ts {
return str;
}

function applyTemplateStringMapping(symbol: Symbol, texts: readonly string[], types: readonly Type[]): [texts: readonly string[], types: readonly Type[]] {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return [texts.map(t => t.toUpperCase()), types.map(t => getStringMappingType(symbol, t))];
case IntrinsicTypeKind.Lowercase: return [texts.map(t => t.toLowerCase()), types.map(t => getStringMappingType(symbol, t))];
case IntrinsicTypeKind.Capitalize: return [texts[0] === "" ? texts : [texts[0].charAt(0).toUpperCase() + texts[0].slice(1), ...texts.slice(1)], texts[0] === "" ? [getStringMappingType(symbol, types[0]), ...types.slice(1)] : types];
case IntrinsicTypeKind.Uncapitalize: return [texts[0] === "" ? texts : [texts[0].charAt(0).toLowerCase() + texts[0].slice(1), ...texts.slice(1)], texts[0] === "" ? [getStringMappingType(symbol, types[0]), ...types.slice(1)] : types];
}
return [texts, types];
}

function getStringMappingTypeForGenericType(symbol: Symbol, type: Type): Type {
const id = `${getSymbolId(symbol)},${getTypeId(type)}`;
let result = stringMappingTypes.get(id);
Expand Down Expand Up @@ -15651,8 +15664,8 @@ namespace ts {
accessNode;
}

function isPatternLiteralPlaceholderType(type: Type) {
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt));
function isPatternLiteralPlaceholderType(type: Type): boolean {
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt)) || !!(type.flags & TypeFlags.StringMapping && isPatternLiteralPlaceholderType((type as StringMappingType).type));
}

function isPatternLiteralType(type: Type) {
Expand Down Expand Up @@ -19613,6 +19626,13 @@ namespace ts {
return Ternary.True;
}
}
else if (target.flags & TypeFlags.StringMapping) {
if (!(source.flags & TypeFlags.StringMapping)) {
if (isMemberOfStringMapping(source, target)) {
return Ternary.True;
}
}
}

if (sourceFlags & TypeFlags.TypeVariable) {
// IndexedAccess comparisons are handled above in the `targetFlags & TypeFlage.IndexedAccess` branch
Expand Down Expand Up @@ -19657,7 +19677,10 @@ namespace ts {
}
}
else if (sourceFlags & TypeFlags.StringMapping) {
if (targetFlags & TypeFlags.StringMapping && (source as StringMappingType).symbol === (target as StringMappingType).symbol) {
if (targetFlags & TypeFlags.StringMapping) {
if ((source as StringMappingType).symbol !== (target as StringMappingType).symbol) {
return Ternary.False;
}
if (result = isRelatedTo((source as StringMappingType).type, (target as StringMappingType).type, RecursionFlags.Both, reportErrors)) {
resetErrorInfo(saveErrorInfo);
return result;
Expand Down Expand Up @@ -20611,7 +20634,7 @@ namespace ts {
}
}

return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral);
return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral) || !!(type.flags & TypeFlags.StringMapping);
}

function getExactOptionalUnassignableProperties(source: Type, target: Type) {
Expand Down Expand Up @@ -22171,6 +22194,32 @@ namespace ts {
&& (!roundTripOnly || s === pseudoBigIntToString({ negative, base10Value: parsePseudoBigInt(scanner.getTokenValue()) }));
}

function isMemberOfStringMapping(source: Type, target: Type): boolean {
if (target.flags & (TypeFlags.String | TypeFlags.AnyOrUnknown)) {
return true;
}
if (target.flags & TypeFlags.TemplateLiteral) {
return isTypeAssignableTo(source, target);
}
if (target.flags & TypeFlags.StringMapping) {
// We need to see whether applying the same mappings of the target
// onto the source would produce an identical type *and* that
// it's compatible with the inner-most non-string-mapped type.
//
// The intuition here is that if same mappings don't affect the source at all,
// and the source is compatible with the unmapped target, then they must
// still reside in the same domain.
const mappingStack = [];
while (target.flags & TypeFlags.StringMapping) {
mappingStack.unshift(target.symbol);
target = (target as StringMappingType).type;
}
const mappedSource = reduceLeft(mappingStack, (memo, value) => getStringMappingType(value, memo), source);
return mappedSource === source && isMemberOfStringMapping(source, target);
}
return false;
}

function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
if (source === target || target.flags & (TypeFlags.Any | TypeFlags.String)) {
return true;
Expand All @@ -22179,7 +22228,8 @@ namespace ts {
const value = (source as StringLiteralType).value;
return !!(target.flags & TypeFlags.Number && isValidNumberString(value, /*roundTripOnly*/ false) ||
target.flags & TypeFlags.BigInt && isValidBigIntString(value, /*roundTripOnly*/ false) ||
target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (target as IntrinsicType).intrinsicName);
target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (target as IntrinsicType).intrinsicName ||
target.flags & TypeFlags.StringMapping && isMemberOfStringMapping(getStringLiteralType(value), target));
}
if (source.flags & TypeFlags.TemplateLiteral) {
const texts = (source as TemplateLiteralType).texts;
Expand Down
16 changes: 8 additions & 8 deletions tests/baselines/reference/intrinsicTypes.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,35 @@ tests/cases/conformance/types/typeAliases/intrinsicTypes.ts(43,5): error TS2322:
==== tests/cases/conformance/types/typeAliases/intrinsicTypes.ts (8 errors) ====
type TU1 = Uppercase<'hello'>; // "HELLO"
type TU2 = Uppercase<'foo' | 'bar'>; // "FOO" | "BAR"
type TU3 = Uppercase<string>; // string
type TU4 = Uppercase<any>; // any
type TU3 = Uppercase<string>; // Uppercase<string>
type TU4 = Uppercase<any>; // Uppercase<`${any}`>
type TU5 = Uppercase<never>; // never
type TU6 = Uppercase<42>; // Error
~~
!!! error TS2344: Type 'number' does not satisfy the constraint 'string'.

type TL1 = Lowercase<'HELLO'>; // "hello"
type TL2 = Lowercase<'FOO' | 'BAR'>; // "foo" | "bar"
type TL3 = Lowercase<string>; // string
type TL4 = Lowercase<any>; // any
type TL3 = Lowercase<string>; // Lowercase<string>
type TL4 = Lowercase<any>; // Lowercase<`${any}`>
type TL5 = Lowercase<never>; // never
type TL6 = Lowercase<42>; // Error
~~
!!! error TS2344: Type 'number' does not satisfy the constraint 'string'.

type TC1 = Capitalize<'hello'>; // "Hello"
type TC2 = Capitalize<'foo' | 'bar'>; // "Foo" | "Bar"
type TC3 = Capitalize<string>; // string
type TC4 = Capitalize<any>; // any
type TC3 = Capitalize<string>; // Capitalize<string>
type TC4 = Capitalize<any>; // Capitalize<`${any}`>
type TC5 = Capitalize<never>; // never
type TC6 = Capitalize<42>; // Error
~~
!!! error TS2344: Type 'number' does not satisfy the constraint 'string'.

type TN1 = Uncapitalize<'Hello'>; // "hello"
type TN2 = Uncapitalize<'Foo' | 'Bar'>; // "foo" | "bar"
type TN3 = Uncapitalize<string>; // string
type TN4 = Uncapitalize<any>; // any
type TN3 = Uncapitalize<string>; // Uncapitalize<string>
type TN4 = Uncapitalize<any>; // Uncapitalize<`${any}`>
type TN5 = Uncapitalize<never>; // never
type TN6 = Uncapitalize<42>; // Error
~~
Expand Down
16 changes: 8 additions & 8 deletions tests/baselines/reference/intrinsicTypes.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
//// [intrinsicTypes.ts]
type TU1 = Uppercase<'hello'>; // "HELLO"
type TU2 = Uppercase<'foo' | 'bar'>; // "FOO" | "BAR"
type TU3 = Uppercase<string>; // string
type TU4 = Uppercase<any>; // any
type TU3 = Uppercase<string>; // Uppercase<string>
type TU4 = Uppercase<any>; // Uppercase<`${any}`>
type TU5 = Uppercase<never>; // never
type TU6 = Uppercase<42>; // Error

type TL1 = Lowercase<'HELLO'>; // "hello"
type TL2 = Lowercase<'FOO' | 'BAR'>; // "foo" | "bar"
type TL3 = Lowercase<string>; // string
type TL4 = Lowercase<any>; // any
type TL3 = Lowercase<string>; // Lowercase<string>
type TL4 = Lowercase<any>; // Lowercase<`${any}`>
type TL5 = Lowercase<never>; // never
type TL6 = Lowercase<42>; // Error

type TC1 = Capitalize<'hello'>; // "Hello"
type TC2 = Capitalize<'foo' | 'bar'>; // "Foo" | "Bar"
type TC3 = Capitalize<string>; // string
type TC4 = Capitalize<any>; // any
type TC3 = Capitalize<string>; // Capitalize<string>
type TC4 = Capitalize<any>; // Capitalize<`${any}`>
type TC5 = Capitalize<never>; // never
type TC6 = Capitalize<42>; // Error

type TN1 = Uncapitalize<'Hello'>; // "hello"
type TN2 = Uncapitalize<'Foo' | 'Bar'>; // "foo" | "bar"
type TN3 = Uncapitalize<string>; // string
type TN4 = Uncapitalize<any>; // any
type TN3 = Uncapitalize<string>; // Uncapitalize<string>
type TN4 = Uncapitalize<any>; // Uncapitalize<`${any}`>
type TN5 = Uncapitalize<never>; // never
type TN6 = Uncapitalize<42>; // Error

Expand Down
16 changes: 8 additions & 8 deletions tests/baselines/reference/intrinsicTypes.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ type TU2 = Uppercase<'foo' | 'bar'>; // "FOO" | "BAR"
>TU2 : Symbol(TU2, Decl(intrinsicTypes.ts, 0, 30))
>Uppercase : Symbol(Uppercase, Decl(lib.es5.d.ts, --, --))

type TU3 = Uppercase<string>; // string
type TU3 = Uppercase<string>; // Uppercase<string>
>TU3 : Symbol(TU3, Decl(intrinsicTypes.ts, 1, 36))
>Uppercase : Symbol(Uppercase, Decl(lib.es5.d.ts, --, --))

type TU4 = Uppercase<any>; // any
type TU4 = Uppercase<any>; // Uppercase<`${any}`>
Copy link
Contributor

@rbuckton rbuckton Apr 7, 2022

Choose a reason for hiding this comment

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

Is there a reason we can't just produce Uppercase<any>? The choice to qualify it as `${any}` seems to be made here: https://github.com/microsoft/TypeScript/pull/47050/files#diff-d9ab6589e714c71e657f601cf30ff51dfc607fc98419bf72e04f6b0fa92cc4b8R15322

Copy link
Member Author

@weswigham weswigham Apr 8, 2022

Choose a reason for hiding this comment

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

Iirc, mostly just because, as written, nongeneric string mapping types can only exist over string, template literals, or other mappings (any open-ended stringy type) - it's a bit easier to work with and check for when the type set they're constructed with is smaller (by explicitly coercing anything else into a template type).

>TU4 : Symbol(TU4, Decl(intrinsicTypes.ts, 2, 29))
>Uppercase : Symbol(Uppercase, Decl(lib.es5.d.ts, --, --))

Expand All @@ -31,11 +31,11 @@ type TL2 = Lowercase<'FOO' | 'BAR'>; // "foo" | "bar"
>TL2 : Symbol(TL2, Decl(intrinsicTypes.ts, 7, 30))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type TL3 = Lowercase<string>; // string
type TL3 = Lowercase<string>; // Lowercase<string>
>TL3 : Symbol(TL3, Decl(intrinsicTypes.ts, 8, 36))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

type TL4 = Lowercase<any>; // any
type TL4 = Lowercase<any>; // Lowercase<`${any}`>
>TL4 : Symbol(TL4, Decl(intrinsicTypes.ts, 9, 29))
>Lowercase : Symbol(Lowercase, Decl(lib.es5.d.ts, --, --))

Expand All @@ -55,11 +55,11 @@ type TC2 = Capitalize<'foo' | 'bar'>; // "Foo" | "Bar"
>TC2 : Symbol(TC2, Decl(intrinsicTypes.ts, 14, 31))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

type TC3 = Capitalize<string>; // string
type TC3 = Capitalize<string>; // Capitalize<string>
>TC3 : Symbol(TC3, Decl(intrinsicTypes.ts, 15, 37))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

type TC4 = Capitalize<any>; // any
type TC4 = Capitalize<any>; // Capitalize<`${any}`>
>TC4 : Symbol(TC4, Decl(intrinsicTypes.ts, 16, 30))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))

Expand All @@ -79,11 +79,11 @@ type TN2 = Uncapitalize<'Foo' | 'Bar'>; // "foo" | "bar"
>TN2 : Symbol(TN2, Decl(intrinsicTypes.ts, 21, 33))
>Uncapitalize : Symbol(Uncapitalize, Decl(lib.es5.d.ts, --, --))

type TN3 = Uncapitalize<string>; // string
type TN3 = Uncapitalize<string>; // Uncapitalize<string>
>TN3 : Symbol(TN3, Decl(intrinsicTypes.ts, 22, 39))
>Uncapitalize : Symbol(Uncapitalize, Decl(lib.es5.d.ts, --, --))

type TN4 = Uncapitalize<any>; // any
type TN4 = Uncapitalize<any>; // Uncapitalize<`${any}`>
>TN4 : Symbol(TN4, Decl(intrinsicTypes.ts, 23, 32))
>Uncapitalize : Symbol(Uncapitalize, Decl(lib.es5.d.ts, --, --))

Expand Down
32 changes: 16 additions & 16 deletions tests/baselines/reference/intrinsicTypes.types
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ type TU1 = Uppercase<'hello'>; // "HELLO"
type TU2 = Uppercase<'foo' | 'bar'>; // "FOO" | "BAR"
>TU2 : "FOO" | "BAR"

type TU3 = Uppercase<string>; // string
>TU3 : string
type TU3 = Uppercase<string>; // Uppercase<string>
>TU3 : Uppercase<string>

type TU4 = Uppercase<any>; // any
>TU4 : any
type TU4 = Uppercase<any>; // Uppercase<`${any}`>
>TU4 : Uppercase<`${any}`>

type TU5 = Uppercase<never>; // never
>TU5 : never
Expand All @@ -23,11 +23,11 @@ type TL1 = Lowercase<'HELLO'>; // "hello"
type TL2 = Lowercase<'FOO' | 'BAR'>; // "foo" | "bar"
>TL2 : "foo" | "bar"

type TL3 = Lowercase<string>; // string
>TL3 : string
type TL3 = Lowercase<string>; // Lowercase<string>
>TL3 : Lowercase<string>

type TL4 = Lowercase<any>; // any
>TL4 : any
type TL4 = Lowercase<any>; // Lowercase<`${any}`>
>TL4 : Lowercase<`${any}`>

type TL5 = Lowercase<never>; // never
>TL5 : never
Expand All @@ -41,11 +41,11 @@ type TC1 = Capitalize<'hello'>; // "Hello"
type TC2 = Capitalize<'foo' | 'bar'>; // "Foo" | "Bar"
>TC2 : "Foo" | "Bar"

type TC3 = Capitalize<string>; // string
>TC3 : string
type TC3 = Capitalize<string>; // Capitalize<string>
>TC3 : Capitalize<string>

type TC4 = Capitalize<any>; // any
>TC4 : any
type TC4 = Capitalize<any>; // Capitalize<`${any}`>
>TC4 : Capitalize<`${any}`>

type TC5 = Capitalize<never>; // never
>TC5 : never
Expand All @@ -59,11 +59,11 @@ type TN1 = Uncapitalize<'Hello'>; // "hello"
type TN2 = Uncapitalize<'Foo' | 'Bar'>; // "foo" | "bar"
>TN2 : "foo" | "bar"

type TN3 = Uncapitalize<string>; // string
>TN3 : string
type TN3 = Uncapitalize<string>; // Uncapitalize<string>
>TN3 : Uncapitalize<string>

type TN4 = Uncapitalize<any>; // any
>TN4 : any
type TN4 = Uncapitalize<any>; // Uncapitalize<`${any}`>
>TN4 : Uncapitalize<`${any}`>

type TN5 = Uncapitalize<never>; // never
>TN5 : never
Expand Down
6 changes: 4 additions & 2 deletions tests/baselines/reference/mappedTypeConstraints2.errors.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts(10,11): error TS2322: Type 'Mapped2<K>[`get${K}`]' is not assignable to type '{ a: K; }'.
Type 'Mapped2<K>[`get${string}`]' is not assignable to type '{ a: K; }'.
tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts(16,11): error TS2322: Type 'Mapped3<K>[Uppercase<K>]' is not assignable to type '{ a: K; }'.
Type 'Mapped3<K>[string]' is not assignable to type '{ a: K; }'.
Type 'Mapped3<K>[Uppercase<string>]' is not assignable to type '{ a: K; }'.
Type 'Mapped3<K>[string]' is not assignable to type '{ a: K; }'.
tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts(25,57): error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.

Expand All @@ -28,7 +29,8 @@ tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts(25,57): error TS2
const x: { a: K } = obj[key]; // Error
~
!!! error TS2322: Type 'Mapped3<K>[Uppercase<K>]' is not assignable to type '{ a: K; }'.
!!! error TS2322: Type 'Mapped3<K>[string]' is not assignable to type '{ a: K; }'.
!!! error TS2322: Type 'Mapped3<K>[Uppercase<string>]' is not assignable to type '{ a: K; }'.
!!! error TS2322: Type 'Mapped3<K>[string]' is not assignable to type '{ a: K; }'.
}

// Repro from #47794
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(7,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<Lowercase<string>>'.
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(15,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(16,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.


==== tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts (3 errors) ====
declare var x: Uppercase<Lowercase<string>>;

// good
x = "A";

// bad
x = "a";
~
!!! error TS2322: Type 'string' is not assignable to type 'Uppercase<Lowercase<string>>'.

declare var y: Uppercase<Lowercase<`${number}`>>;

// good
y = "1";

// bad
y = "a";
~
!!! error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.
y = "A";
~
!!! error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.
Loading