The Art of Type Arguments 9 exercises
explainer

Understand Literal Inference in Generics

TypeScript infers types in function arguments differently depending on what you pass in. Let's look at several examples to understand the nuances.

We'll start with a simple identity function that returns the value that is passed in:

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

co
Loading explainer

Transcript

0:00 It's time to start talking about how TypeScript infers different things in type arguments from things you pass to functions because there's quite a lot here that's nasty and a little bit interesting.

0:12 I've done this in an explainer format because problem solution here is a little bit cruel. Here we've got a function that returns the value only. It just returns whatever you pass in. We've seen this type of function before, and you can see that the result here is actually inferred as A.

0:29 When we return the value inside an object, like we've got T1, T here, then result is actually typed as string. You can see that it's basically exactly the same thing that's going on here. We're just returning T, but here we wrap it in an object, whereas here we just return it. Here it gets typed as string, whereas up here it gets typed as its literal value.

0:56 Inside here we returnsValueInAnObjectWithConstraint. We've added extends string inside here, and now result3 gets typed as its literal value again. What? What? That's mad, right? The reason I think this happens is this seems relatively inconsistent to me. It feels like in the compiler, you have a special case where it sees, OK, if you're just returning that thing, then you should infer it as its literal type.

1:30 This means that if we get like 1 here, it's going to infer it as 1. Whereas if you put it inside an object that's potentially could be mutated afterwards, result2.t equals blah, blah, blah, blah, blah, then this it feels like it should work.

1:50 Whereas it feels like here we're hitting maybe another special case where if you infer it to be a specific string, you can't pass numbers into here for instance, then it's going to yell at you. All of these options, so result3, we can't really reassign it. We can assign it to abc, but we can't assign it to another random string.

2:14 Each of these solutions here, it feels like TypeScript is trying to be as helpful as possible, but he fact that they're inconsistent from each other will throw you off. There are some levers you can pull. If you're just doing a basic identity function, then TypeScript will pretty much always refer, infer the thing that you pass in, although it won't do it deeply, which we'll see in a later explainer.

2:40 If you're returning the value in an object then, or like an array for instance too, so I can actually put T inside here, then it makes sense that this is a string array, as opposed to if I add a constraint to this, T1 extends string, then this will actually put it inside abc here. Now inside result2, I can't push anything that isn't abc. I can't push a random string to it.

3:05 Or maybe I can restart the TS server. This will yell at me, yeah. It's not assignable to parameter or type abc. You can use these constraints to basically tighten TypeScript, and make sure it's inferring the literal inside there. Sometimes the literal is exactly what you want.

3:25 Sometimes you're going to get this more basic inference, where you can basically get something like this, return the value inside an object, and sometimes this will be what you want as well. This should give you a sense that TypeScript is giving you...is trying to be as helpful as it can and you can tweak the constraints or tweak the return values as to what it's typed in, to make it make more sense.