Skip to content

Commit 7d2272d

Browse files
ljharbclaude
andcommitted
Narrow String#toLowerCase/toUpperCase return types via Lowercase/Uppercase
Make String.prototype.toLowerCase and toUpperCase preserve string-literal types in their return type, by typing them as `<T extends string>(this: T): Lowercase<T>` and `Uppercase<T>` respectively. For non-literal `string` receivers, `Lowercase<string>` resolves to `string`, preserving existing behavior. For literal receivers (e.g. `"FOO".toLowerCase()`), the result narrows to the corresponding literal (`"foo"`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f350b52 commit 7d2272d

64 files changed

Lines changed: 734 additions & 492 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/lib/es5.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,13 +481,13 @@ interface String {
481481
substring(start: number, end?: number): string;
482482

483483
/** Converts all the alphabetic characters in a string to lowercase. */
484-
toLowerCase(): string;
484+
toLowerCase<T extends string>(this: T): string extends T ? string : Lowercase<T>;
485485

486486
/** Converts all alphabetic characters to lowercase, taking into account the host environment's current locale. */
487487
toLocaleLowerCase(locales?: string | string[]): string;
488488

489489
/** Converts all the alphabetic characters in a string to uppercase. */
490-
toUpperCase(): string;
490+
toUpperCase<T extends string>(this: T): string extends T ? string : Uppercase<T>;
491491

492492
/** Returns a string where all alphabetic characters have been converted to uppercase, taking into account the host environment's current locale. */
493493
toLocaleUpperCase(locales?: string | string[]): string;

tests/baselines/reference/abstractPropertyInConstructor.types

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ abstract class AbstractClass {
3232
> : ^^^^^^
3333
>this.prop.toLowerCase() : string
3434
> : ^^^^^^
35-
>this.prop.toLowerCase : () => string
36-
> : ^^^^^^
35+
>this.prop.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
36+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
3737
>this.prop : string
3838
> : ^^^^^^
3939
>this : this
4040
> : ^^^^
4141
>prop : string
4242
> : ^^^^^^
43-
>toLowerCase : () => string
44-
> : ^^^^^^
43+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
44+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
4545

4646
if (!str) {
4747
>!str : boolean
@@ -215,16 +215,16 @@ abstract class DerivedAbstractClass extends AbstractClass {
215215
> : ^ ^^ ^^^^^^^^^
216216
>this.prop.toLowerCase() : string
217217
> : ^^^^^^
218-
>this.prop.toLowerCase : () => string
219-
> : ^^^^^^
218+
>this.prop.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
219+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
220220
>this.prop : string
221221
> : ^^^^^^
222222
>this : this
223223
> : ^^^^
224224
>prop : string
225225
> : ^^^^^^
226-
>toLowerCase : () => string
227-
> : ^^^^^^
226+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
227+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
228228

229229
this.method(1);
230230
>this.method(1) : void

tests/baselines/reference/arrayconcat.errors.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
arrayconcat.ts(12,9): error TS2564: Property 'options' has no initializer and is not definitely assigned in the constructor.
2+
arrayconcat.ts(16,25): error TS2684: The 'this' context of type 'string | undefined' is not assignable to method's 'this' of type 'string'.
3+
Type 'undefined' is not assignable to type 'string'.
24
arrayconcat.ts(16,25): error TS18048: 'a.name' is possibly 'undefined'.
5+
arrayconcat.ts(17,25): error TS2684: The 'this' context of type 'string | undefined' is not assignable to method's 'this' of type 'string'.
6+
Type 'undefined' is not assignable to type 'string'.
37
arrayconcat.ts(17,25): error TS18048: 'b.name' is possibly 'undefined'.
48

59

6-
==== arrayconcat.ts (3 errors) ====
10+
==== arrayconcat.ts (5 errors) ====
711
interface IOptions {
812
name?: string;
913
flag?: boolean;
@@ -23,9 +27,15 @@ arrayconcat.ts(17,25): error TS18048: 'b.name' is possibly 'undefined'.
2327
this.options = this.options.sort(function(a, b) {
2428
var aName = a.name.toLowerCase();
2529
~~~~~~
30+
!!! error TS2684: The 'this' context of type 'string | undefined' is not assignable to method's 'this' of type 'string'.
31+
!!! error TS2684: Type 'undefined' is not assignable to type 'string'.
32+
~~~~~~
2633
!!! error TS18048: 'a.name' is possibly 'undefined'.
2734
var bName = b.name.toLowerCase();
2835
~~~~~~
36+
!!! error TS2684: The 'this' context of type 'string | undefined' is not assignable to method's 'this' of type 'string'.
37+
!!! error TS2684: Type 'undefined' is not assignable to type 'string'.
38+
~~~~~~
2939
!!! error TS18048: 'b.name' is possibly 'undefined'.
3040

3141
if (aName > bName) {

tests/baselines/reference/arrayconcat.types

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,32 +78,32 @@ class parser {
7878
> : ^^^^^^
7979
>a.name.toLowerCase() : string
8080
> : ^^^^^^
81-
>a.name.toLowerCase : () => string
82-
> : ^^^^^^
81+
>a.name.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
82+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
8383
>a.name : string | undefined
8484
> : ^^^^^^^^^^^^^^^^^^
8585
>a : IOptions
8686
> : ^^^^^^^^
8787
>name : string | undefined
8888
> : ^^^^^^^^^^^^^^^^^^
89-
>toLowerCase : () => string
90-
> : ^^^^^^
89+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
90+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
9191

9292
var bName = b.name.toLowerCase();
9393
>bName : string
9494
> : ^^^^^^
9595
>b.name.toLowerCase() : string
9696
> : ^^^^^^
97-
>b.name.toLowerCase : () => string
98-
> : ^^^^^^
97+
>b.name.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
98+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
9999
>b.name : string | undefined
100100
> : ^^^^^^^^^^^^^^^^^^
101101
>b : IOptions
102102
> : ^^^^^^^^
103103
>name : string | undefined
104104
> : ^^^^^^^^^^^^^^^^^^
105-
>toLowerCase : () => string
106-
> : ^^^^^^
105+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
106+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
107107

108108
if (aName > bName) {
109109
>aName > bName : boolean

tests/baselines/reference/bestChoiceType.types

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@
3232
> : ^^^^^^
3333
>s.toLowerCase() : string
3434
> : ^^^^^^
35-
>s.toLowerCase : () => string
36-
> : ^^^^^^
35+
>s.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
36+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
3737
>s : string
3838
> : ^^^^^^
39-
>toLowerCase : () => string
40-
> : ^^^^^^
39+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
40+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
4141

4242
// Similar cases
4343

@@ -86,12 +86,12 @@ function f1() {
8686
> : ^^^^^^
8787
>s.toLowerCase() : string
8888
> : ^^^^^^
89-
>s.toLowerCase : () => string
90-
> : ^^^^^^
89+
>s.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
90+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
9191
>s : string
9292
> : ^^^^^^
93-
>toLowerCase : () => string
94-
> : ^^^^^^
93+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
94+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
9595
}
9696

9797
function f2() {
@@ -141,11 +141,11 @@ function f2() {
141141
> : ^^^^^^
142142
>s.toLowerCase() : string
143143
> : ^^^^^^
144-
>s.toLowerCase : () => string
145-
> : ^^^^^^
144+
>s.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
145+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
146146
>s : string
147147
> : ^^^^^^
148-
>toLowerCase : () => string
149-
> : ^^^^^^
148+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
149+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
150150
}
151151

tests/baselines/reference/checkJsdocSatisfiesTag13.types

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ const t1 = { f: s => s.toLowerCase() }; // should work
1515
> : ^^^^^^
1616
>s.toLowerCase() : string
1717
> : ^^^^^^
18-
>s.toLowerCase : () => string
19-
> : ^^^^^^
18+
>s.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
19+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
2020
>s : string
2121
> : ^^^^^^
22-
>toLowerCase : () => string
23-
> : ^^^^^^
22+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
23+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
2424

2525
/** @satisfies {{ f: (x: string) => string }} */
2626
const t2 = { g: "oops" }; // should error

tests/baselines/reference/commaOperatorWithSecondOperandObjectType.types

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,12 @@ STRING.toLowerCase(), new CLASS()
182182
> : ^^^^^
183183
>STRING.toLowerCase() : string
184184
> : ^^^^^^
185-
>STRING.toLowerCase : () => string
186-
> : ^^^^^^
185+
>STRING.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
186+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
187187
>STRING : string
188188
> : ^^^^^^
189-
>toLowerCase : () => string
190-
> : ^^^^^^
189+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
190+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
191191
>new CLASS() : CLASS
192192
> : ^^^^^
193193
>CLASS : typeof CLASS
@@ -274,12 +274,12 @@ var resultIsObject11 = (STRING.toLowerCase(), new CLASS());
274274
> : ^^^^^
275275
>STRING.toLowerCase() : string
276276
> : ^^^^^^
277-
>STRING.toLowerCase : () => string
278-
> : ^^^^^^
277+
>STRING.toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
278+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
279279
>STRING : string
280280
> : ^^^^^^
281-
>toLowerCase : () => string
282-
> : ^^^^^^
281+
>toLowerCase : <T extends string>(this: T) => string extends T ? string : Lowercase<T>
282+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
283283
>new CLASS() : CLASS
284284
> : ^^^^^
285285
>CLASS : typeof CLASS

0 commit comments

Comments
 (0)