The Art of Type Arguments 9 exercises
explainer

Understand Generic Inference When Using Objects as Arguments

Let's talk about about inference in generics some more.

Here we have the acceptsValueOnly function that we saw earlier. It returns the exact result of what you pass in as well as the literal result:

const acceptsValueOnly = <T>(t: T) => {
  return t
}

// const result: "a"
const result =
Loading explainer

Transcript

0:00 You can never talk enough about generic inference. Let's talk a bit more. We've got this acceptsValueOnly function, which we saw in another explainer, which returns the exact result of what you pass in and the literal result too.

0:13 If you pass that wrapped in an object inside here, then it's going to actually...so inside here acceptsValueInAnObject. We've got our T, we've got our object input, and the input is where we're doing the generic inference, then it's typed as string instead.

0:31 If we do this in an as const, so same function as we can see, acceptsValueInAnObject, but we actually make this as const, then this is going to actually infer it as abc instead. That's because if we extract this out here, so const, asConstObj equals this, if we pass that in, then we can see that this is actually inferred as abc here. It's read-only too.

0:59 It makes sense that inside here, then it captures this in the slot, whereas it's interesting that it doesn't do it by default there. Fine, TypeScript. You do you. acceptsValueInAnObjectFieldWithConstraints. Similar to the previous explainer, we're now constraining this type here.

1:21 We're saying input extends string. You can't pass numbers into here, then again, result3 is now typed as abc. Again, this constraint is helping us refine the thing that's being passed in and helping TypeScript infer it as its literal value. That's pretty nice. This one acceptsValueWithObjectConstraint.

1:42 This time, the T is actually like...it's not representing the actual input that's being passed in, it's representing the entire object. This is really interesting difference. Instead now, when we call this previous one, like this call, we can see that this is being grabbed in the type argument, which is the actual string itself, so abc is being grabbed.

2:06 Whereas when we call this one with the same input, then the entire object is being grabbed there. You can see that we do actually have a constraint on here. We can say T extends input string. Because the constraint is not actually on the input itself, it's on the entire object, then TypeScript doesn't actually infer the input there.

2:28 It doesn't infer the literal. This doesn't work with as const either. This is really weird. It's like where you put the generic inference actually matters for what gets inferred, and whether it gets inferred as a literal or not. In other words, if the entire object is in that slot, then it's not going to be inferred as well as if you just infer the literal value on there. That's the difference.

2:55 You have this input T where T extends string, or T if I make this like TObject instead, because it doesn't really matter the name of it, TObject extends input string where the entire object is the thing that's in the type argument.

3:12 The further you get away from the thing that you're actually trying to infer, the more you try to infer in those slots, the worse the inference is going to be. There are some exceptions to that, which we'll look at in the future, but this is really key to understand, that the choices of what you represent in these type arguments do matter for TypeScript's inference.