TypeScript
TypeScript copied to clipboard
Generic param that extends mapped type behaves differently when cast to the same type
Bug Report
In a generic function, a param that extends a mapped type doesn't properly support the type it extends. Casting such param explicitly to the same type that it already extends, surprisingly, makes it properly support the type it extends.
🔎 Search Terms
ts2322,2322- Mapped type extends cast
🕗 Version & Regression Information
Verified on 4.8.4, 4.9.3, and 5.0.0-dev.20221127
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about
keyofand found nothing related to what I am observing
⏯ Playground Link
Playground link with relevant code
💻 Code
type RequireStringField<T, TKey extends keyof T> = {
[P in TKey]: string;
};
function myFunc<T extends RequireStringField<T, TKey>, TKey extends keyof T>(
obj: T,
field: TKey
) {
// This should work, but doesn't (tested on Typescript 4.8.4):
obj[field] = "modified"; // ❌: Type 'string' is not assignable to type 'T[TKey]'. (ts2322)
// This works, even though the cast would seem like a superfluous no-op...
// (we are casting `obj` to the type it already extends):
(obj as RequireStringField<T, TKey>)[field] = "modified"; // ✅: works
}
myFunc({field1: "a", field2: 1}, "field1"); // ✅: works
The error is correct. The only guarantee made for T is that it's assignable to RequireStringField<T, TKey>, which means the properties involved might be narrower than string:
type Oops = { foo: "foo" | "bar" };
const oops: Oops = { foo: "bar" };
myFunc(oops, "foo"); // legal call - no error
console.log(oops.foo); // type is "foo" | "bar" but prints "modified"
@fatcerberus thanks for looking at this! I see what you mean, but this raises another can of worms. Please see this snippet below. Comments:
CODE MARKER 1: if I got your comment right, then that line should also raise a compiler error. Yet it doesn'tCODE MARKER 2: looks like the compiler just allowed an unsafe operation to go through. Likely becauseCODE MARKER 1really should have produced an error
So now there are two things to look at:
- Help advise what might be a good way to achieve what I intended in case that error is indeed legit -- how to declare a function that expects a given field of an object to be of a specific type
- Review the related bug that the snippet below highlights
function myFunc<T extends { field1: string }>(obj: T) {
// CODE MARKER 1: per fatcerberus's comment, the following should also produce ts2322. But it compiles just fine
obj.field1 = "modified";
}
type Oops = { field1: "foo" | "bar" };
const oops: Oops = { field1: "bar" };
// CODE MARKER 2: `myFunc` is about to set `oops.field1` to an unacceptable value. And the compiler didn't produce any errors
myFunc(oops);
Good catch; Marker 1 should be an error for the same reason the equivalent line in the OP is. Not sure what's going on there. Might be you've found an actual bug.
on a side note:
I'll note for the record that { field: "foo" | "bar" } being assignable to { field: string } at all is inherently unsound, as objects are passed by reference, but this loophole exists because it's quite common in real-world code to use narrower objects in calls that ask for a wider type, but less common to mutate them across call boundaries. TS is only stricter with generics because writing a generic is an acknowledgement that the actual type of a value will vary, so the compiler tries to ensure that it's valid for all such types. tl;dr, "having a string property" is not the same thing as "having a property whose value is assignable to string", as the former implies assignability in both directions.
The error is less about writing to a field with a type that matches the constraint, but rather that the value might not even align as a supertype of the constraint at all when the function is called with a union type as the key. In this case there's a RequireStringField constraint that prevents that, but TS doesn't realize that this has an impact on anything.
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.