Home > Software design >  Narrow number argument of function to be literal type
Narrow number argument of function to be literal type

Time:09-16

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 intellisense suggesting type Partial & {id: 3}

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}

error message on id property

how should I write my function to get the proper narrowing I'm looking for?

playground link

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.

Playground link to code

  • Related