Repository navigation
Any reason why DANGEROUS operations like this work and surprisingly does not work in ONLY one edge case?Β #55763
Description
π Search Terms
"generics", "generics type constraints", "generics record index key value change"
π Version & Regression Information
- This is a crash
β― Playground Link
π» Code
// 1. Scenario - A Dangerous operation which TS allows
function mutate1(obj: Record<string, unknown>) {
obj.key = 1; // this is dangerous. No error
}
const obj1 = {key: 'some key'};
mutate1(obj1); // Should TS error out here as it errors out when passing a readonly to non readonly
console.log(obj1.key.toLocaleLowerCase()); // EXCEPTION: key is now number NOT string.
// -------------------------- END ---------
// 2. Scenario - Here TS prevents error from happening but don't know why
function mutate2<Args extends Record<string, unknown>>(obj: Args) {
// Why does this error out?
obj.key = 1; // This errors out. So NOTHING can be mutated here
if('key' in obj && typeof obj.key === 'number') {
// NO obj keys can EVER be changed not matter the typeguard. Why????
obj.key = 2; // It still error's out inspite of all the typeguard
}
}
const obj2 = {key: 'some key'};
mutate2(obj2);
console.log(obj2.key.toLocaleLowerCase()); // key WILL be string here. PERFECT
// -------------------------- END ---------
/**
* 3.
*
* Following Scenario 2. This surprisingly DOES NOT prevent error from happening
* don't know why
*/
function mutate3<Args extends Record<string, unknown>[]>(obj: Args) {
if(obj[0]) {
obj[0].key = 1; // This DOES NOT errors out. Whyyy?
}
}
const obj3 = [{key: 'some key'}];
mutate3(obj3);
console.log(obj3[0]?.key.toLocaleLowerCase()); // EXCEPTION: key is now number NOT string.
// -------------------------- END ---------
// 5. Scenario - this works as expected
const readonlyArr: readonly string[] = ['first'];
const mutatingArr: unknown[] = readonlyArr; // This errors out correctly
// --------- END -----
// 4. Scenario - can easily change readonly object keys
const readonlyObj = {k: 2} as const;
const mutatingObj: Record<string, unknown> = readonlyObj;
mutatingObj.k = 'string'; // No error here as well
readonlyObj.k.toFixed(2); // This WILL THROW as 'k' is no longer a number now
// ------------ END ------------π Actual behavior
1] If a function accepts an argument of type Record<string, unknown> then the function can change the value of any key to ANY value. While this makes sense within the function, but, if you pass a well structured object to this method as an argument then the function can change any key to any value and that will result in runtime error and there is no compile time error when a well structured object is passed to a function accepting Record<string, unknown>. But, if you pass a readonly array to non readonly array then it errors out properly.
2] If a function accepts the same argument type Record<string, unknown> via generics then changing ANY keys of the object within the function is not possible.
3] If the function accepts the argument of type Record<string, unknown>[] this time as a array then the function can change any key of any object within the array. This is in stark contrast to point 2 where it is not possible.
4] Readonly array cannot be assigned to non readonly array but readonly object CAN be assigned to non readonly object. Why this difference in functionality?
π Expected behavior
1] Readonly object should NOT be assignable to Record<string, unknown> as any keys can be mutated within them causing dangerous side effects.
2] The behaviour of Record<string, unknown> should be the same regardless of whether it is enforced via generics or directly as a function argument type.
3] Generics constraint Record<string, unknown>[] i.e. array variant (here any key of any object within the array can be changed) does not seem to behave like generic constraint enforced via Record<string, unknown> i.e. non array variant (here no object keys can be changed)
Additional information about the issue
No response
Activity
MartinJohns commentedon Sep 16, 2023
Duplicate of #13347. Readonly objects are implicitly compatible with the mutable versions.
abaldawa commentedon Sep 16, 2023
Martin Johns (@MartinJohns) I have highlighted many other points besides that just one point. It is not a full duplicate of the #13347, only one point matches with the issues I have highlighted
whzx5byb commentedon Sep 16, 2023
This is a well-known unsoundness: A writable property should be invariant but is considered covariant by default. See also #55451 and comments.
And it is very unlikely to be "fixed" because the team treats it as "trade-off" for usability. See more discussions: #9825.
MartinJohns commentedon Sep 16, 2023
Also, fyi, this is not a crash. "Crash" refers to when the compiler crashes.
typescript-bot commentedon Sep 21, 2023
This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
isti115 commentedon Sep 23, 2023
Abhijit (@abaldawa) The reason why read-only arrays are not assignable to mutable arrays is explained by #13347 (comment):
(Note that
readonly string[]is just syntax forReadonlyArray<string>:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#a-new-syntax-for-readonlyarray)