It's unsafe for the same reason that this is unsafe: the caller chooses the type parameter, which can be narrower than the generic constraint, and one way to make a type narrower is to narrow its property types. So the type Shirt
defined as
type Shirt = { size: "S" | "M" | "L", color: string }
const redShirt: Shirt = { size: "L", color: "red" };
is a subtype of StrKeyStrVal
:
const strKeyStrVal: StrKeyStrVal = redShirt; // okay
and that's a problem for you, because the size
property is strictly narrower than string
, and if you're not careful you'll assign some unacceptable string
to it:
setKeyVal(redShirt, "size", "XXXXXL"); // oops
So, what can you do? If you don't expect to run into the above situation you can just a type assertion and move on. If you want to avoid type assertions, you need to refactor.
One way to refactor which is just as unsafe as an assertion is to remove the generics:
function setKeyVal(obj: StrKeyStrVal, key: string, value: string) {
obj[key] = value; // okay
}
setKeyVal(redShirt, "size", "XXXXXL"); // oops!
That's because TypeScript is unsafe in places, and non-generic supertype property writing is one such place. See microsoft/TypeScript#14150 for discussion about this. The conclusion of that issue is that it would be incredibly annoying for many people to use TypeScript if it didn't allow property writes to supertypes. So productivity wins out over type safety here. That might be what you want in this case.
One safer way to refactor is to make the function sufficiently generic that it will only accept the right strings for the right keys:
function setKeyVal<T extends StrKeyStrVal, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value; // okay now
}
The implementation is now seen as safe, and the bad call above with redShirt
now throws an error at the call site:
setKeyVal(redShirt, "size", "XXXXXL"); // error!
// -----------------------> ~~~~~~~~
// Argument of type '"XXXXXL"' is not assignable to parameter of type '"S" | "M" | "L"'
Now we have a function which is harder to use incorrectly, but is also harder to use and harder to annotate. You get more type safety, but at some cost.
I guess it depends on your use cases and your priorities which way you want to proceed. In any case I hope this helps make sense of the situation. Good luck!
Playground link to code