Skip to content

Commit c10ff7d

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 c10ff7d

64 files changed

Lines changed: 986 additions & 840 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): 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): 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: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,20 @@ abstract class AbstractClass {
2828
> : ^^^^^^
2929

3030
let val = this.prop.toLowerCase();
31-
>val : string
32-
> : ^^^^^^
33-
>this.prop.toLowerCase() : string
34-
> : ^^^^^^
35-
>this.prop.toLowerCase : () => string
36-
> : ^^^^^^
31+
>val : Lowercase<string>
32+
> : ^^^^^^^^^^^^^^^^^
33+
>this.prop.toLowerCase() : Lowercase<string>
34+
> : ^^^^^^^^^^^^^^^^^
35+
>this.prop.toLowerCase : <T extends string>(this: T) => 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) => Lowercase<T>
44+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
4545

4646
if (!str) {
4747
>!str : boolean
@@ -213,18 +213,18 @@ abstract class DerivedAbstractClass extends AbstractClass {
213213
> : ^^^^
214214
>cb : (s: string) => void
215215
> : ^ ^^ ^^^^^^^^^
216-
>this.prop.toLowerCase() : string
217-
> : ^^^^^^
218-
>this.prop.toLowerCase : () => string
219-
> : ^^^^^^
216+
>this.prop.toLowerCase() : Lowercase<string>
217+
> : ^^^^^^^^^^^^^^^^^
218+
>this.prop.toLowerCase : <T extends string>(this: T) => 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) => 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: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,44 +74,44 @@ class parser {
7474
> : ^^^^^^^^
7575

7676
var aName = a.name.toLowerCase();
77-
>aName : string
78-
> : ^^^^^^
79-
>a.name.toLowerCase() : string
80-
> : ^^^^^^
81-
>a.name.toLowerCase : () => string
82-
> : ^^^^^^
77+
>aName : Lowercase<string>
78+
> : ^^^^^^^^^^^^^^^^^
79+
>a.name.toLowerCase() : Lowercase<string>
80+
> : ^^^^^^^^^^^^^^^^^
81+
>a.name.toLowerCase : <T extends string>(this: T) => 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) => Lowercase<T>
90+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
9191

9292
var bName = b.name.toLowerCase();
93-
>bName : string
94-
> : ^^^^^^
95-
>b.name.toLowerCase() : string
96-
> : ^^^^^^
97-
>b.name.toLowerCase : () => string
98-
> : ^^^^^^
93+
>bName : Lowercase<string>
94+
> : ^^^^^^^^^^^^^^^^^
95+
>b.name.toLowerCase() : Lowercase<string>
96+
> : ^^^^^^^^^^^^^^^^^
97+
>b.name.toLowerCase : <T extends string>(this: T) => 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) => Lowercase<T>
106+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
107107

108108
if (aName > bName) {
109109
>aName > bName : boolean
110110
> : ^^^^^^^
111-
>aName : string
112-
> : ^^^^^^
113-
>bName : string
114-
> : ^^^^^^
111+
>aName : Lowercase<string>
112+
> : ^^^^^^^^^^^^^^^^^
113+
>bName : Lowercase<string>
114+
> : ^^^^^^^^^^^^^^^^^
115115

116116
return 1;
117117
>1 : 1
@@ -120,10 +120,10 @@ class parser {
120120
} else if (aName < bName) {
121121
>aName < bName : boolean
122122
> : ^^^^^^^
123-
>aName : string
124-
> : ^^^^^^
125-
>bName : string
126-
> : ^^^^^^
123+
>aName : Lowercase<string>
124+
> : ^^^^^^^^^^^^^^^^^
125+
>bName : Lowercase<string>
126+
> : ^^^^^^^^^^^^^^^^^
127127

128128
return -1;
129129
>-1 : -1

tests/baselines/reference/bestChoiceType.types

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
// Repro from #10041
55

66
(''.match(/ /) || []).map(s => s.toLowerCase());
7-
>(''.match(/ /) || []).map(s => s.toLowerCase()) : string[]
8-
> : ^^^^^^^^
7+
>(''.match(/ /) || []).map(s => s.toLowerCase()) : Lowercase<string>[]
8+
> : ^^^^^^^^^^^^^^^^^^^
99
>(''.match(/ /) || []).map : (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[]) | (<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])
1010
> : ^^ ^^ ^^^ ^^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^ ^^ ^^^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^
1111
>(''.match(/ /) || []) : RegExpMatchArray | []
@@ -26,18 +26,18 @@
2626
> : ^^
2727
>map : (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[]) | (<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])
2828
> : ^^ ^^ ^^^ ^^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^ ^^ ^^^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^
29-
>s => s.toLowerCase() : (s: string) => string
30-
> : ^ ^^^^^^^^^^^^^^^^^^^
29+
>s => s.toLowerCase() : (s: string) => Lowercase<string>
30+
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3131
>s : string
3232
> : ^^^^^^
33-
>s.toLowerCase() : string
34-
> : ^^^^^^
35-
>s.toLowerCase : () => string
36-
> : ^^^^^^
33+
>s.toLowerCase() : Lowercase<string>
34+
> : ^^^^^^^^^^^^^^^^^
35+
>s.toLowerCase : <T extends string>(this: T) => Lowercase<T>
36+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
3737
>s : string
3838
> : ^^^^^^
39-
>toLowerCase : () => string
40-
> : ^^^^^^
39+
>toLowerCase : <T extends string>(this: T) => Lowercase<T>
40+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
4141

4242
// Similar cases
4343

@@ -70,28 +70,28 @@ function f1() {
7070
> : ^^
7171

7272
let z = y.map(s => s.toLowerCase());
73-
>z : string[]
74-
> : ^^^^^^^^
75-
>y.map(s => s.toLowerCase()) : string[]
76-
> : ^^^^^^^^
73+
>z : Lowercase<string>[]
74+
> : ^^^^^^^^^^^^^^^^^^^
75+
>y.map(s => s.toLowerCase()) : Lowercase<string>[]
76+
> : ^^^^^^^^^^^^^^^^^^^
7777
>y.map : (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[]) | (<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])
7878
> : ^^ ^^ ^^^ ^^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^ ^^ ^^^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^
7979
>y : RegExpMatchArray | []
8080
> : ^^^^^^^^^^^^^^^^^^^^^
8181
>map : (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[]) | (<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])
8282
> : ^^ ^^ ^^^ ^^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^ ^^ ^^^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^
83-
>s => s.toLowerCase() : (s: string) => string
84-
> : ^ ^^^^^^^^^^^^^^^^^^^
83+
>s => s.toLowerCase() : (s: string) => Lowercase<string>
84+
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8585
>s : string
8686
> : ^^^^^^
87-
>s.toLowerCase() : string
88-
> : ^^^^^^
89-
>s.toLowerCase : () => string
90-
> : ^^^^^^
87+
>s.toLowerCase() : Lowercase<string>
88+
> : ^^^^^^^^^^^^^^^^^
89+
>s.toLowerCase : <T extends string>(this: T) => Lowercase<T>
90+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
9191
>s : string
9292
> : ^^^^^^
93-
>toLowerCase : () => string
94-
> : ^^^^^^
93+
>toLowerCase : <T extends string>(this: T) => Lowercase<T>
94+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
9595
}
9696

9797
function f2() {
@@ -125,27 +125,27 @@ function f2() {
125125
> : ^^^^^^^
126126

127127
let z = y.map(s => s.toLowerCase());
128-
>z : string[]
129-
> : ^^^^^^^^
130-
>y.map(s => s.toLowerCase()) : string[]
131-
> : ^^^^^^^^
128+
>z : Lowercase<string>[]
129+
> : ^^^^^^^^^^^^^^^^^^^
130+
>y.map(s => s.toLowerCase()) : Lowercase<string>[]
131+
> : ^^^^^^^^^^^^^^^^^^^
132132
>y.map : (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[]) | (<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])
133133
> : ^^ ^^ ^^^ ^^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^ ^^ ^^^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^
134134
>y : RegExpMatchArray | never[]
135135
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^
136136
>map : (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[]) | (<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])
137137
> : ^^ ^^ ^^^ ^^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^ ^^ ^^^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^
138-
>s => s.toLowerCase() : (s: string) => string
139-
> : ^ ^^^^^^^^^^^^^^^^^^^
138+
>s => s.toLowerCase() : (s: string) => Lowercase<string>
139+
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
140140
>s : string
141141
> : ^^^^^^
142-
>s.toLowerCase() : string
143-
> : ^^^^^^
144-
>s.toLowerCase : () => string
145-
> : ^^^^^^
142+
>s.toLowerCase() : Lowercase<string>
143+
> : ^^^^^^^^^^^^^^^^^
144+
>s.toLowerCase : <T extends string>(this: T) => Lowercase<T>
145+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
146146
>s : string
147147
> : ^^^^^^
148-
>toLowerCase : () => string
149-
> : ^^^^^^
148+
>toLowerCase : <T extends string>(this: T) => Lowercase<T>
149+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
150150
}
151151

tests/baselines/reference/checkJsdocSatisfiesTag13.types

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@
33
=== /a.js ===
44
/** @satisfies {{ f: (x: string) => string }} */
55
const t1 = { f: s => s.toLowerCase() }; // should work
6-
>t1 : { f: (s: string) => string; }
7-
> : ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
8-
>{ f: s => s.toLowerCase() } : { f: (s: string) => string; }
9-
> : ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
10-
>f : (s: string) => string
11-
> : ^ ^^^^^^^^^^^^^^^^^^^
12-
>s => s.toLowerCase() : (s: string) => string
13-
> : ^ ^^^^^^^^^^^^^^^^^^^
6+
>t1 : { f: (s: string) => Lowercase<string>; }
7+
> : ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8+
>{ f: s => s.toLowerCase() } : { f: (s: string) => Lowercase<string>; }
9+
> : ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10+
>f : (s: string) => Lowercase<string>
11+
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
>s => s.toLowerCase() : (s: string) => Lowercase<string>
13+
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1414
>s : string
1515
> : ^^^^^^
16-
>s.toLowerCase() : string
17-
> : ^^^^^^
18-
>s.toLowerCase : () => string
19-
> : ^^^^^^
16+
>s.toLowerCase() : Lowercase<string>
17+
> : ^^^^^^^^^^^^^^^^^
18+
>s.toLowerCase : <T extends string>(this: T) => Lowercase<T>
19+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
2020
>s : string
2121
> : ^^^^^^
22-
>toLowerCase : () => string
23-
> : ^^^^^^
22+
>toLowerCase : <T extends string>(this: T) => Lowercase<T>
23+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
2424

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

tests/baselines/reference/commaOperatorWithSecondOperandObjectType.types

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,14 @@ true, {}
180180
STRING.toLowerCase(), new CLASS()
181181
>STRING.toLowerCase(), new CLASS() : CLASS
182182
> : ^^^^^
183-
>STRING.toLowerCase() : string
184-
> : ^^^^^^
185-
>STRING.toLowerCase : () => string
186-
> : ^^^^^^
183+
>STRING.toLowerCase() : Lowercase<string>
184+
> : ^^^^^^^^^^^^^^^^^
185+
>STRING.toLowerCase : <T extends string>(this: T) => Lowercase<T>
186+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
187187
>STRING : string
188188
> : ^^^^^^
189-
>toLowerCase : () => string
190-
> : ^^^^^^
189+
>toLowerCase : <T extends string>(this: T) => Lowercase<T>
190+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
191191
>new CLASS() : CLASS
192192
> : ^^^^^
193193
>CLASS : typeof CLASS
@@ -272,14 +272,14 @@ var resultIsObject11 = (STRING.toLowerCase(), new CLASS());
272272
> : ^^^^^
273273
>STRING.toLowerCase(), new CLASS() : CLASS
274274
> : ^^^^^
275-
>STRING.toLowerCase() : string
276-
> : ^^^^^^
277-
>STRING.toLowerCase : () => string
278-
> : ^^^^^^
275+
>STRING.toLowerCase() : Lowercase<string>
276+
> : ^^^^^^^^^^^^^^^^^
277+
>STRING.toLowerCase : <T extends string>(this: T) => Lowercase<T>
278+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
279279
>STRING : string
280280
> : ^^^^^^
281-
>toLowerCase : () => string
282-
> : ^^^^^^
281+
>toLowerCase : <T extends string>(this: T) => Lowercase<T>
282+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
283283
>new CLASS() : CLASS
284284
> : ^^^^^
285285
>CLASS : typeof CLASS

0 commit comments

Comments
 (0)