Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
57 changes: 50 additions & 7 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12168,7 +12168,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 @@ -15367,8 +15367,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)) :
Copy link
Member

Choose a reason for hiding this comment

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

The fact that you have to spread the arguments in, ugh.

type;
}

Expand All @@ -15382,6 +15385,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 @@ -15637,8 +15650,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 @@ -19594,6 +19607,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 @@ -19638,7 +19658,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 @@ -20592,7 +20615,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 @@ -22149,6 +22172,25 @@ 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) {
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);
Copy link
Member

Choose a reason for hiding this comment

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

You could just use push and reduceRight, no? I guess we might not define our own reduceRight.

Also, would be nice to have a comment. Consider:

Suggested change
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);
// 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.push(target.symbol);
target = (target as StringMappingType).type;
}
const mappedSource = mappingStack.reduceRight((memo, value) => getStringMappingType(value, memo), source);

Copy link
Member

Choose a reason for hiding this comment

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

I agree, a comment like that would make it clearer what's going on.

Copy link
Member Author

Choose a reason for hiding this comment

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

You could just use push and reduceRight, no? I guess we might not define our own reduceRight.

Yep, that's the reason. We only have a reduceLeft and not a reduceRight utility function, and I usually prefer to use those if they're available (otherwise we should remove them right?). Given this array will likely never be more than a handful of elements long, my preference would be to keep the core helper in use, rather than use the (es6) array method. Less work for that one guy patching the TS compiler to run on a blackberry phone browser.

return mappedSource === source && isMemberOfStringMapping(source, target);
Copy link
Member

Choose a reason for hiding this comment

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

It's too bad that it's not clearer that target is modified, otherwise it looks at first glance like it'll result in runaway recursion.

}
return false;
}

function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
if (source === target || target.flags & (TypeFlags.Any | TypeFlags.String)) {
return true;
Expand All @@ -22157,7 +22199,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