Skip to content

Improve error message for index signature on generic type when writing #55906

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 6 commits into from
Oct 19, 2023
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
7 changes: 6 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17845,7 +17845,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (indexInfo) {
if (accessFlags & AccessFlags.NoIndexSignatures && indexInfo.keyType !== numberType) {
if (accessExpression) {
error(accessExpression, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(originalObjectType));
if (accessFlags & AccessFlags.Writing) {
error(accessExpression, Diagnostics.Type_0_is_generic_and_can_only_be_indexed_for_reading, typeToString(originalObjectType));
}
else {
error(accessExpression, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(originalObjectType));
}
}
return undefined;
}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3699,6 +3699,10 @@
"category": "Error",
"code": 2861
},
"Type '{0}' is generic and can only be indexed for reading.": {
Copy link
Member

Choose a reason for hiding this comment

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

I know we hate the existing message about "T could be instantiated with a different subtype", buuuuuuuuuuuut..what about:

Type {property access} cannot be used to index '{type variable}' because '{type variable}' could be instantiated with an index signature that is a subtype of '{index signature}'.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know if/how adding that explanation helps people... Also, mentioning the index type makes it so you get two almost identical error messages if the index type is a union (e.g. string | symbol), which is why Anders suggested above to omit the index type entirely.
Why do we hate the existing "T could be instantiated with a different subtype"?

Copy link
Member Author

Choose a reason for hiding this comment

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

According to Ryan, we added the elaboration "T could be instantiated with a different subtype" because people didn't understand exactly why they were getting a (general) assignability error, and it helped people at least in the sense they had a more specific error message they could look up to try and understand the rule. I don't feel it's the same here, the message is already specific about T being generic. Do you think the extra information here would help people who don't understand the issue already?

"category": "Error",
"code": 2862
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
cannotIndexGenericWritingError.ts(4,5): error TS2862: Type 'T' is generic and can only be indexed for reading.
cannotIndexGenericWritingError.ts(8,5): error TS2862: Type 'T' is generic and can only be indexed for reading.


==== cannotIndexGenericWritingError.ts (2 errors) ====
// From #47357

function foo<T extends Record<string | symbol, any>>(target: T, p: string | symbol) {
target[p] = ""; // error
~~~~~~~~~
!!! error TS2862: Type 'T' is generic and can only be indexed for reading.
}

function foo2<T extends number[] & { [s: string]: number | string }>(target: T, p: string | number) {
target[p] = 1; // error
~~~~~~~~~
!!! error TS2862: Type 'T' is generic and can only be indexed for reading.
target[1] = 1; // ok
}
33 changes: 33 additions & 0 deletions tests/baselines/reference/cannotIndexGenericWritingError.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//// [tests/cases/compiler/cannotIndexGenericWritingError.ts] ////

=== cannotIndexGenericWritingError.ts ===
// From #47357

function foo<T extends Record<string | symbol, any>>(target: T, p: string | symbol) {
>foo : Symbol(foo, Decl(cannotIndexGenericWritingError.ts, 0, 0))
>T : Symbol(T, Decl(cannotIndexGenericWritingError.ts, 2, 13))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>target : Symbol(target, Decl(cannotIndexGenericWritingError.ts, 2, 53))
>T : Symbol(T, Decl(cannotIndexGenericWritingError.ts, 2, 13))
>p : Symbol(p, Decl(cannotIndexGenericWritingError.ts, 2, 63))

target[p] = ""; // error
>target : Symbol(target, Decl(cannotIndexGenericWritingError.ts, 2, 53))
>p : Symbol(p, Decl(cannotIndexGenericWritingError.ts, 2, 63))
}

function foo2<T extends number[] & { [s: string]: number | string }>(target: T, p: string | number) {
>foo2 : Symbol(foo2, Decl(cannotIndexGenericWritingError.ts, 4, 1))
>T : Symbol(T, Decl(cannotIndexGenericWritingError.ts, 6, 14))
>s : Symbol(s, Decl(cannotIndexGenericWritingError.ts, 6, 38))
>target : Symbol(target, Decl(cannotIndexGenericWritingError.ts, 6, 69))
>T : Symbol(T, Decl(cannotIndexGenericWritingError.ts, 6, 14))
>p : Symbol(p, Decl(cannotIndexGenericWritingError.ts, 6, 79))

target[p] = 1; // error
>target : Symbol(target, Decl(cannotIndexGenericWritingError.ts, 6, 69))
>p : Symbol(p, Decl(cannotIndexGenericWritingError.ts, 6, 79))

target[1] = 1; // ok
>target : Symbol(target, Decl(cannotIndexGenericWritingError.ts, 6, 69))
}
38 changes: 38 additions & 0 deletions tests/baselines/reference/cannotIndexGenericWritingError.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//// [tests/cases/compiler/cannotIndexGenericWritingError.ts] ////

=== cannotIndexGenericWritingError.ts ===
// From #47357

function foo<T extends Record<string | symbol, any>>(target: T, p: string | symbol) {
>foo : <T extends Record<string | symbol, any>>(target: T, p: string | symbol) => void
>target : T
>p : string | symbol

target[p] = ""; // error
>target[p] = "" : ""
>target[p] : any
>target : T
>p : string | symbol
>"" : ""
}

function foo2<T extends number[] & { [s: string]: number | string }>(target: T, p: string | number) {
>foo2 : <T extends number[] & { [s: string]: string | number; }>(target: T, p: string | number) => void
>s : string
>target : T
>p : string | number

target[p] = 1; // error
>target[p] = 1 : 1
>target[p] : any
>target : T
>p : string | number
>1 : 1

target[1] = 1; // ok
>target[1] = 1 : 1
>target[1] : number
>target : T
>1 : 1
>1 : 1
}
4 changes: 2 additions & 2 deletions tests/baselines/reference/keyofAndIndexedAccess2.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ keyofAndIndexedAccess2.ts(52,3): error TS2322: Type 'number' is not assignable t
keyofAndIndexedAccess2.ts(53,3): error TS2322: Type 'number' is not assignable to type 'T[K]'.
'T[K]' could be instantiated with an arbitrary type which could be unrelated to 'number'.
keyofAndIndexedAccess2.ts(65,7): error TS2339: Property 'foo' does not exist on type 'T'.
keyofAndIndexedAccess2.ts(66,3): error TS2536: Type 'string' cannot be used to index type 'T'.
keyofAndIndexedAccess2.ts(66,3): error TS2862: Type 'T' is generic and can only be indexed for reading.
keyofAndIndexedAccess2.ts(67,3): error TS2322: Type 'number' is not assignable to type 'T[keyof T]'.
'number' is assignable to the constraint of type 'T[keyof T]', but 'T[keyof T]' could be instantiated with a different subtype of constraint 'number'.
keyofAndIndexedAccess2.ts(68,3): error TS2322: Type 'number' is not assignable to type 'T[K]'.
Expand Down Expand Up @@ -146,7 +146,7 @@ keyofAndIndexedAccess2.ts(108,5): error TS2322: Type '123' is not assignable to
!!! error TS2339: Property 'foo' does not exist on type 'T'.
obj[k1] = 123; // Error
~~~~~~~
!!! error TS2536: Type 'string' cannot be used to index type 'T'.
!!! error TS2862: Type 'T' is generic and can only be indexed for reading.
obj[k2] = 123; // Error
~~~~~~~
!!! error TS2322: Type 'number' is not assignable to type 'T[keyof T]'.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mappedTypeGenericWithKnownKeys.ts(9,9): error TS2551: Property 'unknownLiteralKey' does not exist on type 'Record<keyof Shape | "knownLiteralKey", number>'. Did you mean 'knownLiteralKey'?
mappedTypeGenericWithKnownKeys.ts(10,5): error TS2536: Type 'string' cannot be used to index type 'Record<keyof Shape | "knownLiteralKey", number>'.
mappedTypeGenericWithKnownKeys.ts(10,5): error TS2862: Type 'Record<keyof Shape | "knownLiteralKey", number>' is generic and can only be indexed for reading.


==== mappedTypeGenericWithKnownKeys.ts (2 errors) ====
Expand All @@ -16,6 +16,6 @@ mappedTypeGenericWithKnownKeys.ts(10,5): error TS2536: Type 'string' cannot be u
!!! error TS2551: Property 'unknownLiteralKey' does not exist on type 'Record<keyof Shape | "knownLiteralKey", number>'. Did you mean 'knownLiteralKey'?
obj['' as string] = 4; // error
~~~~~~~~~~~~~~~~~
!!! error TS2536: Type 'string' cannot be used to index type 'Record<keyof Shape | "knownLiteralKey", number>'.
!!! error TS2862: Type 'Record<keyof Shape | "knownLiteralKey", number>' is generic and can only be indexed for reading.
}

13 changes: 13 additions & 0 deletions tests/cases/compiler/cannotIndexGenericWritingError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @strict: true
// @noEmit: true

// From #47357

function foo<T extends Record<string | symbol, any>>(target: T, p: string | symbol) {
target[p] = ""; // error
}

function foo2<T extends number[] & { [s: string]: number | string }>(target: T, p: string | number) {
target[p] = 1; // error
target[1] = 1; // ok
}