I'm writing a method that takes a id:number
and an partial object that must contain id
, with the SAME value as the 1st argument.
here's what I'm thinking
type MyData = {id: number}
function update<K extends MyData['id']>(id: K, newValues: Partial<MyData> & {id: K}): MyData {
throw 'not implemented'
}
// expected failure?
update(3, {id: 4})
what's weird is the intellisense seems to agree with my logic but it's not enforced
if I extract that type in the intellisense suggestion, and try to use it, I do get the failure I expected when calling the function
type ArgType = Partial<MyData> & {id: 3}
const argProof: ArgType = {id: 4}
how should I write my function to get the proper narrowing I'm looking for?
CodePudding user response:
Your intent was that when you call update()
, the compiler should infer the generic type parameter from the argument passed as the id
parameter, and then check the value passed in as the newValues
parameter against it. But the compiler had other plans; it decided to infer the type parameter from both locations, and synthesized a union type:
update(3, { id: 4 });
// function update<3 | 4>(id: 3 | 4, newValues: Partial<MyData> & { id: 3 | 4; }): MyData
That's not wrong, per se; one could imagine someone else wanting exactly this behavior for a function of the same call signature. (Frankly I'm a little surprised that this happened, since the common complaint I see is the opposite, where people want a union but the compiler refuses; see Why isn't the type argument inferred as a union type?. But I'm not going to focus on figuring out why.) It's not what you want, though.
Again, your intent was that the appearance of the type parameter inside newValues
was just for checking and not for inference. There is a request at microsoft/TypeScript#14829 for such "non-inferential type parameter usage", where you'd mark the type parameter T
with NoInfer<T>
and it would block inference while leaving everything else alone. Like this:
function update<T extends MyData['id']>(
id: T,
newValues: Partial<MyData> & { id: NoInfer<T> }
): MyData {
throw 'not implemented'
}
Now, there's no direct support for this. But the GitHub issue does mention several ways of emulating this behavior. One such approach is to "lower the priority" of the inference site by intersecting it with {}
(which doesn't do anything to non-nullable types):
type NoInfer<T> = T & {};
If you do this then things behave as you desire:
update(3, { id: 4 }); // error! 4 is not assignable to 3
update(3, { id: 3 }); // okay
There are other approaches mentioned in there, like adding another generic type parameter constrained to the first:
function update<T extends MyData['id'], U extends T>(
id: T,
newValues: Partial<MyData> & { id: U }
): MyData {
throw 'not implemented'
}
update(3, { id: 4 }); // error! 4 is not assignable to 3
update(3, { id: 3 }); // okay
The different approaches have their pros and cons. For more information you should consult the issue in GitHub.