-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Using keyof with key remapping to exclude index signatures doesn't return known keys #41966
Comments
|
I just ran into the following situation which was not resolved by #41713. Below, type MapKnownKeys<T> = {
[K in keyof T as string extends K ? never : number extends K ? never : K]: string;
}
interface Foo {
a: number,
b: number,
[k: string]: unknown;
}
type MappedFoo = MapKnownKeys<Foo>;
/* type MappedFoo = {
a: string;
b: string;
} */
const mappedFoo: MappedFoo = {
a: "",
b: "",
}; // okay
type KeysOfMappedFoo = keyof MappedFoo;
// type KeysOfMappedFoo = never 💥
const a: KeysOfMappedFoo = "a"; // error!
// ~ <-- Type 'string' is not assignable to type 'never' |
@RyanCavanaugh Why is it legal to index it with both The implicit coercion from numbers from strings is usually fine in situations that are "contravariant" in the key type (e.g. indexing), but it ends up producing all kinds of nonsense that needs to be casted/ EDIT: Wow, apparently TypeScript doesn't prevent |
Simply because |
@weswigham Hmm. I thought about it a bit and I'm still having trouble seeing why it follows that allowing I think what you're trying to get me to understand is that |
Making you write |
@RyanCavanaugh I mean you said it yourself:
What it accomplishes is that |
Run |
I tried:
But I don't really see anything I didn't expect. Could you handhold me through what you're saying? |
Specifically, I don't understand why the result above means rejecting:
as type errors is a problem. |
There's nothing unsound about accepting that program, but we can reject it on an arbitrary "looks wrong" basis like we do for other uses. The question is then to what end: What kinds of mistakes are being prevented? What is gained? There are theoretical consistency arguments to be made in both directions but just arguments from gut feel are unlikely to motivate changing behavior that dates back to TS's very first release. |
A property with the name |
That's right, and yet they show up in the result of
See #25260 for a couple examples of "covariant" usage of keys that are broken by the implicit |
The motivating example is function read<T>(obj: T, key: keyof T) {
return obj[key];
}
// Should be OK because `[1,2,3][1]` is OK
read([1, 2, 3], 1); At this point we have to start arguing whether |
@RyanCavanaugh You're using the key contravariantly, and to quote myself above: things are "usually fine in situations that are "contravariant" in the key type". Try a "covariant" usage in which you actually extract the keys from the object. For example: declare const keys: <T extends { [k: string]: unknown }>(t: T) => (keyof T)[]
declare const randomString: () => string
keys({ foo: 1, bar: 2 }) // "foo" | "bar"
// Good!
keys({ [randomString()]: 1, [randomString()]: 2 }) // string | number
// wuh-oh |
Right. So the options are:
|
Don't really get where that first option is coming from. function read<T>(obj: T, key: keyof T) {
return obj[key];
}
type MyArray<T> = { arrayMethod: string, [k: number]: T }
declare const myArray: MyArray<boolean>
const thisIsAString = myArray.arrayMethod
const thisIsABoolean = myArray[1]
const thisIsAUnionOfThingsJustLikeInYourReadExample = read(myArray, 1) Like, |
If number slots don't exist at runtime and there's a desire to model accessing those "numeric" strings: shouldn't it be the other way around? For example, the current behaviour is: type numberIndex = keyof { [k: number]: unknown }; // number
type stringIndex = keyof { [k: string]: unknown }; // string | number but shouldn't it be: type numberIndex = keyof { [k: number]: unknown }; // string | number
type stringIndex = keyof { [k: string]: unknown }; // string Of course, while I understand the use case, which is certainly valid, IMO I'm not so sure if the benefits of modelling either of the behaviours I just mentioned outweigh the loss of type information. Considering that given the current behaviour; a covariant usage like in @masaeedu's example above; and the fact that numbers are actually coerced to strings at runtime when used to index objects like arrays; it often creates a scenario where there's no way to know that "number" should be stripped from the resulting type. |
If you have declare const m: { prop: boolean };
m["prop"]; // OK
m[0]; // Not OK
m["zero"]; // Not OK If you have declare const m: { [k: number]: boolean };
m[0]; // OK
m["zero"]; // Not OK If you have declare const m: { [k: string]: boolean };
m[0]; // OK, per prior discussion
m["zero"]; // Also OK You can argue that You can also argue that |
Which line, if any, should be an error in this program under this rule? If no errors, how would we justify this as reasonable behavior? function read<T>(obj: T, key: keyof T) {
return obj[key];
}
const arr: { [n: number]: number } = [1, 2, 3];
const m: number = read(arr, "not an array index at all"); |
This seems a bit circular. We're saying that Wouldn't it make more sense for |
The behavior of
This is shuffling the inconsistency around, not removing it. You're proposing that higher-order forms behave differently from lower-order forms, which is a very common source of complaint in other areas. |
There's (at least) two different operations that we want to perform with the keys of an object in JS: indexing an object with them, and enumerating them. I don't know which one is the higher-order form and which one is the lower one: personally I give them equal standing. Given the same object, the types of the keys involved in each operation are related, but differ in a subtle way (because the indexing operation can accept numbers and will implicitly coerce them to strings). You're saying we've simply defined
In other words If |
If someone implemented that rule, I'd expect there to be no errors (note that I'm not actually proposing that). I believe it's arguably justifiable using the same logic applied to the current behaviour: because numbers are coerced to strings. Would it be a good idea to allow indexing such an object with arbitrary string values? No, I don't think so (I think we can all agree there). However, we need to ask ourselves why we believe it's okay to index the same object with arbitrary number values? I understand that TypeScript isn't aiming to be a perfectly sound type system and that it's intended to model existing/common JavaScript patterns, so sure we can justify that we want to allow the current behaviour using that argument. Still, we can reuse that same argument around the other way again! We could argue the other way around is different, since arbitrary strings are not coerced to numbers, but don't forget there are no number indexes in the runtime value. This (and your comment quoted below) leads me to my next point, which I think is along the same lines of what @masaeedu has just raised, regarding "keyof" having a dual purpose: for both indexing, and enumerating keys.
It looks to me like we should ask ourselves (though maybe you already know the answer to this), what was the actual/original purpose of "keyof" intended to be? Does the current behaviour align with its true meaning, and would we be ok with changing the behaviour if we conclude that it doesn't? Or perhaps we change its purpose? Or if it does, should we look at creating a new operator for correctly enumerating keys? |
Couldn't we at least agree that the following example (using latest nightly build) shows that the current behaviour is inconsistent and unintuitive? type Compute<A> = { [K in keyof A]: Compute<A[K]> } & {};
type EqualsTest<T> = <A>() => A extends T ? 1 : 0;
type Equals<A1, A2> = EqualsTest<A2> extends EqualsTest<A1> ? 1 : 0;
type Filter<K, I> = Equals<K, I> extends 1 ? never : K;
type OmitIndex<T, I extends string | number> = {
[K in keyof T as Filter<K, I>]: T[K];
};
type OmitIndex2<T, I extends string | number> = I extends unknown
? {
[K in keyof T as Filter<K, I>]: T[K];
}
: never;
type StringIndexObject = { [key: string]: {} };
type NumberIndexObject = { [key: number]: {} };
type IndexObject = Compute<StringIndexObject & NumberIndexObject>;
type FooBar = { foo: "hello"; bar: "world" };
type FooBar0 = { foo: "hello"; bar: "world"; 0: "0" };
type FooBar1 = { foo: "hello"; bar: "world"; "1": 1 };
type FooBar01 = { foo: "hello"; bar: "world"; 0: "0"; "1": 1 };
type FooBarKey = Compute<keyof FooBar>; // "foo" | "bar"
type FooBar0Key = Compute<keyof FooBar0>; // 0 | "foo" | "bar"
type FooBar1Key = Compute<keyof FooBar1>; // "foo" | "bar" | "1"
type FooBar01Key = Compute<keyof FooBar01>; // 0 | "foo" | "bar" | "1" FooBar
// Actual: { [x: string]: {}; foo: "hello"; bar: "world"; }
// OK
type FooBarWithStringIndex = Compute<FooBar & StringIndexObject>;
// Actual: { foo: "hello"; bar: "world"; }
// OK
type FooBarWithoutStringIndex = OmitIndex<FooBarWithStringIndex, string>;
// Actual: string | number
// Expected: string
type FooBarKeyWithStringIndex = Compute<keyof FooBarWithStringIndex>;
// Actual: number | "foo" | "bar"
// Expected: "foo" | "bar"
type FooBarKeyWithoutStringIndex = Compute<keyof FooBarWithoutStringIndex>; // Actual: { [x: number]: {}; foo: "hello"; bar: "world"; }
// OK
type FooBarWithNumberIndex = Compute<FooBar & NumberIndexObject>;
// Actual: { foo: "hello"; bar: "world"; }
// OK
type FooBarWithoutNumberIndex = OmitIndex<FooBarWithNumberIndex, number>;
// Actual: number | "foo" | "bar"
// OK
type FooBarKeyWithNumberIndex = Compute<keyof FooBarWithNumberIndex>;
// Actual: "foo" | "bar"
// OK
type FooBarKeyWithoutNumberIndex = Compute<keyof FooBarWithoutNumberIndex>; // Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; }
// OK
type FooBarWithIndex = Compute<FooBar & IndexObject>;
// Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; }
// Expected: { foo: "hello"; bar: "world"; }
type FooBarWithoutIndex1 = OmitIndex<FooBarWithIndex, string | number>;
// Actual: | { [x: number]: {}; foo: "hello"; bar: "world"; }
// | { [x: string]: {}; foo: "hello"; bar: "world"; }
// OK
type FooBarWithoutIndex2 = OmitIndex2<FooBarWithIndex, string | number>;
// Actual: { foo: "hello"; bar: "world"; }
// OK
type FooBarWithoutIndex3 = OmitIndex<OmitIndex<FooBarWithIndex, string>, number>;
// Actual: { foo: "hello"; bar: "world"; }
// OK
type FooBarWithoutIndex4 = OmitIndex<OmitIndex<FooBarWithIndex, number>, string>; // Actual: string | number
// OK
type FooBarKeyWithIndex = Compute<keyof FooBarWithIndex>;
// Actual: string | number
// Expected: "foo" | "bar"
type FooBarKeyWithoutIndex1 = Compute<keyof FooBarWithoutIndex1>;
// Actual: "foo" | "bar"
// OK
type FooBarKeyWithoutIndex2 = Compute<keyof FooBarWithoutIndex2>;
// Actual: "foo" | "bar"
// OK
type FooBarKeyWithoutIndex3 = Compute<keyof FooBarWithoutIndex3>;
// Actual: "foo" | "bar"
// OK
type FooBarKeyWithoutIndex4 = Compute<keyof FooBarWithoutIndex4>; FooBar0
// Actual: { [x: string]: {}; foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithStringIndex = Compute<FooBar0 & StringIndexObject>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithoutStringIndex = OmitIndex<FooBar0WithStringIndex, string>;
// Actual: string | n
// Expected: string | 0
type FooBar0KeyWithStringIndex = Compute<keyof FooBar0WithStringIndex>;
// Actual: number | "foo" | "bar"
// Expected: 0 | "foo" | "bar"
type FooBar0KeyWithoutStringIndex = Compute<keyof FooBar0WithoutStringIndex>; // Actual: { [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithNumberIndex = Compute<FooBar0 & NumberIndexObject>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithoutNumberIndex = OmitIndex<FooBar0WithNumberIndex, number>;
// Actual: number | "foo" | "bar"
// OK
type FooBar0KeyWithNumberIndex = Compute<keyof FooBar0WithNumberIndex>;
// Actual: "foo" | "bar"
// OK
type FooBar0KeyWithoutNumberIndex = Compute<keyof FooBar0WithoutNumberIndex>; // Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithIndex = Compute<FooBar0 & IndexObject>;
// Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; }
// Expected: { foo: "hello"; bar: "world"; 0: "0"; }
type FooBar0WithoutIndex1 = OmitIndex<FooBar0WithIndex, string | number>;
// Actual: | { [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; }
// | { [x: string]: {}; foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithoutIndex2 = OmitIndex2<FooBar0WithIndex, string | number>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithoutIndex3 = OmitIndex<OmitIndex<FooBar0WithIndex, string>, number>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; }
// OK
type FooBar0WithoutIndex4 = OmitIndex<OmitIndex<FooBar0WithIndex, number>, string>; // Actual: string | number
// OK
type FooBar0KeyWithIndex = Compute<keyof FooBar0WithIndex>;
// Actual: string | number
// Expected: 0 | "foo" | "bar"
type FooBar0KeyWithoutIndex1 = Compute<keyof FooBar0WithoutIndex1>;
// Actual: 0 | "foo" | "bar"
// OK
type FooBar0KeyWithoutIndex2 = Compute<keyof FooBar0WithoutIndex2>;
// Actual: "foo" | "bar"
// Expected: 0 | "foo" | "bar"
type FooBar0KeyWithoutIndex3 = Compute<keyof FooBar0WithoutIndex3>;
// Actual: 0
// Expected: 0 | "foo" | "bar"
type FooBar0KeyWithoutIndex4 = Compute<keyof FooBar0WithoutIndex4>; FooBar1
// Actual: { [x: string]: {}; foo: "hello"; bar: "world"; 1: 1; }
// Expected: { [x: string]: {}; foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithStringIndex = Compute<FooBar1 & StringIndexObject>;
// Actual: { foo: "hello"; bar: "world"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithoutStringIndex = OmitIndex<FooBar1WithStringIndex, string>;
// Actual: string | number
// Expected: string
type FooBar1KeyWithStringIndex = Compute<keyof FooBar1WithStringIndex>;
// Actual: number | "foo" | "bar" | "1"
// Expected: "foo" | "bar" | "1"
type FooBar1KeyWithoutStringIndex = Compute<keyof FooBar1WithoutStringIndex>; // Actual: { [x: number]: {}; foo: "hello"; bar: "world"; 1: 1; }
// Expected: { [x: number]: {}; foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithNumberIndex = Compute<FooBar1 & NumberIndexObject>;
// Actual: { foo: "hello"; bar: "world"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithoutNumberIndex = OmitIndex<FooBar1WithNumberIndex, number>;
// Actual: number | "foo" | "bar" | "1"
// OK
type FooBar1KeyWithNumberIndex = Compute<keyof FooBar1WithNumberIndex>;
// Actual: "foo" | "bar" | "1"
// OK
type FooBar1KeyWithoutNumberIndex = Compute<keyof FooBar1WithoutNumberIndex>; // Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; 1: 1; }
// Expected: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithIndex = Compute<FooBar1 & IndexObject>;
// Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithoutIndex1 = OmitIndex<FooBar1WithIndex, string | number>;
// Actual: | { [x: number]: {}; foo: "hello"; bar: "world"; 1: 1; }
// | { [x: string]: {}; foo: "hello"; bar: "world"; 1: 1; }
// Expected: | { [x: number]: {}; foo: "hello"; bar: "world"; "1": 1; }
// | { [x: string]: {}; foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithoutIndex2 = OmitIndex2<FooBar1WithIndex, string | number>;
// Actual: { foo: "hello"; bar: "world"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithoutIndex3 = OmitIndex<OmitIndex<FooBar1WithIndex, string>, number>;
// Actual: { foo: "hello"; bar: "world"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; "1": 1; }
type FooBar1WithoutIndex4 = OmitIndex<OmitIndex<FooBar1WithIndex, number>, string>; // Actual: string | number
// OK
type FooBar1KeyWithIndex = Compute<keyof FooBar1WithIndex>;
// Actual: string | number
// Expected: "foo" | "bar" | "1"
type FooBar1KeyWithoutIndex1 = Compute<keyof FooBar1WithoutIndex1>;
// Actual: "foo" | "bar" | "1"
// OK
type FooBar1KeyWithoutIndex2 = Compute<keyof FooBar1WithoutIndex2>;
// Actual: "foo" | "bar" | "1"
// OK
type FooBar1KeyWithoutIndex3 = Compute<keyof FooBar1WithoutIndex3>;
// Actual: "foo" | "bar" | "1"
// OK
type FooBar1KeyWithoutIndex4 = Compute<keyof FooBar1WithoutIndex4>; FooBar01
// Actual: { [x: string]: {}; foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: { [x: string]: {}; foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithStringIndex = Compute<FooBar01 & StringIndexObject>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithoutStringIndex = OmitIndex<FooBar01WithStringIndex, string>;
// Actual: string | number
// Expected: string | 0
type FooBar01KeyWithStringIndex = Compute<keyof FooBar01WithStringIndex>;
// Actual: number | "foo" | "bar" | "1"
// Expected: 0 | "foo" | "bar" | "1"
type FooBar01KeyWithoutStringIndex = Compute<keyof FooBar01WithoutStringIndex>; // Actual: { [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: { [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithNumberIndex = Compute<FooBar01 & NumberIndexObject>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; 1: 1 }
// Expected: { foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithoutNumberIndex = OmitIndex<FooBar01WithNumberIndex, number>;
// Actual: number | "foo" | "bar" | "1"
// OK
type FooBar01KeyWithNumberIndex = Compute<keyof FooBar01WithNumberIndex>;
// Actual: "foo" | "bar" | "1"
// Expected: 0 | "foo" | "bar" | "1"
type FooBar01KeyWithoutNumberIndex = Compute<keyof FooBar01WithoutNumberIndex>; // Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithIndex = Compute<FooBar01 & IndexObject>;
// Actual: { [x: string]: {}; [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithoutIndex1 = OmitIndex<FooBar01WithIndex, string | number>;
// Actual: | { [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// | { [x: string]: {}; foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: | { [x: number]: {}; foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
// | { [x: string]: {}; foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithoutIndex2 = OmitIndex2<FooBar01WithIndex, string | number>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithoutIndex3 = OmitIndex<OmitIndex<FooBar01WithIndex, string>, number>;
// Actual: { foo: "hello"; bar: "world"; 0: "0"; 1: 1; }
// Expected: { foo: "hello"; bar: "world"; 0: "0"; "1": 1; }
type FooBar01WithoutIndex4 = OmitIndex<OmitIndex<FooBar01WithIndex, number>, string>; // Actual: string | number
// OK
type FooBar01KeyWithIndex = Compute<keyof FooBar01WithIndex>;
// Actual: string | number
// Expected: 0 | "foo" | "bar" | "1"
type FooBar01KeyWithoutIndex1 = Compute<keyof FooBar01WithoutIndex1>;
// Actual: 0 | "foo" | "bar" | "1"
// OK
type FooBar01KeyWithoutIndex2 = Compute<keyof FooBar01WithoutIndex2>;
// Actual: "foo" | "bar" | "1"
// Expected: 0 | "foo" | "bar" | "1"
type FooBar01KeyWithoutIndex3 = Compute<keyof FooBar01WithoutIndex3>;
// Actual: 0
// Expected: 0 | "foo" | "bar" | "1"
type FooBar01KeyWithoutIndex4 = Compute<keyof FooBar01WithoutIndex4>; |
Any behavior is going to be "inconsistent" if you squint at it from the right angle since numeric index signatures are purely a fiction used to denote a rough class of objects with common behavior, rather than a real runtime distinction. |
TypeScript Version: 4.2.0-dev.20201211
Search Terms:
key remapping
remap index signature
keyof returns number
Code
Expected behavior:
Using
keyof
with the above remapped type (WithoutIndex
) should return the known keys (i.e."foo" | "bar"
).Actual behavior:
While the key remapping in
OmitIndex
appears to successfully omit the string index signature (e.g.OmitIndex<WithIndex, string> --> { foo: "hello"; bar: "world"; }
); usingkeyof
with the resulting type returnsnumber
.Playground Link:
https://www.typescriptlang.org/play?ts=4.2.0-dev.20201211#code/C4TwDgpgBAwg9gWzAV2BAPAQQHxQLxQDeUA2gNJQCWAdlANYQhwBmUmAugFyyIppbl22ANxQAvlABkRMcIBQc0JCgBRAI7IAhgBsAzgBUIu4On24CWbAAoAlPlyYoEAB5pqAE11R9UAPxQARihuAAZ5JWh1LT0sAIAaNgAmc1UNHQMjE0xkp1cIDy8o9MNjWNx-IND5RXBoADFKbTQAJ3QyBIBJFKKY9qgu3LdPQL8oaggANwhm4KgyaoioAHkESmAOjxdTTsH84eNmmgBzKAAfMeQEACNplMI5KFIKGnpGFm8oTS8Gpum2zuwXG8gnksgUiw27hcSyuACsIABjYD4IikBggbgHY5A5DUOjUOAAd2oojBizqcDgACFNDMCMRmJTuAAiAAWEG02jgzNEV1pLMJcGa2ncPPEC1qUAA6mtWZCXCj4EhUBgKdTaVJ+ptnDD4UiRI8oAB6I2okjOTHAQ7UI5AwiyKCMuAs9mc7m8-lQZmC4Wi0lQdAAWkDyzINWUMuArLgqHlzhRKzWcfQkbl2oSWJtBpNqKdLo5XLFfOaAqFIrFEkNVer1aDIaWYfD9UpNOaZEYKPR7zVrdENZrOeZTuZZy9xZHjzroab0tlcfbIE7b1YqbjferOczJ3O1EuNxmU5UzkgSIg7kt1pOM9TMfW2oXS6YK9lt7XxtNu+u037hsPx8RaDnl6w6jsy45yEAA
Related Issues:
#31143
#41383
#38646
The text was updated successfully, but these errors were encountered: