Add Generic Constraints to Type Helpers
We'll start by taking a closer look at the concept of generic constraints in TypeScript.
Imagine we have a runtime function toUndefinedObject
, which accepts a type parameter T
and returns an empty object:
const toUndefinedObject = (t) => { return {};}
If we want to const
Transcript
00:00 Generic constraints. Let's imagine that toUndefinedObject here was in fact a runtime function which took in a T and returned an empty object, let's say.
00:10 So here then, you notice that T, we need to give it something in order to make sure that what we're passing it can be constrained or is constrained to something. So toUndefinedObject, we don't want to be able to pass it a string or a number or anything like that.
00:24 We only want to be able to pass it a, I think the type that most corresponds to this is actually the object type, lowercase object type. Lowercase object kind of represents sort of any object really, and so we can now pass it an object with a bunch of different properties or things like that,
00:41 and we can't pass it things like numbers, things like undefined, things like null. So this object looks like a pretty good constraint for this T, but how do we actually map it onto this T up here onto the toUndefinedObject type helper?
00:57 How do we constrain this? Because you notice that we have to pass something to this one, we have to annotate it somehow, but with this one we can actually just leave it bare, we don't have to annotate it. But we can using the extends keyword, so T extends object.
01:13 This behaves exactly the same way as this constraint here, where now if we say typeExample equals toUndefinedObject and we pass it in a string, now it's going to yell at us because typeString does not satisfy the constraint object.
01:31 Same idea. So object is working okay. Now though we've still got all of these errors down here, we've still got this error here. Why is that happening? Well, it's because all or nothing, this one is still unconstrained. And so this means that type T does not satisfy the constraint object.
01:48 When you don't have a constraint on something, it defaults it to unknown, or TypeScript treats it as if it were unknown. And so we need to constrain this one as well. So we need to say extends object here too. Now everything starts working because this all or nothing type is properly constrained.
02:06 Like we now get proper errors if we don't pass in the right thing here. But this object type is not quite perfect. And in fact, when I was trying to research this, I didn't actually find a perfect solution for this. The reason it's not perfect is that you can actually pass in like arrays of things. So you can pass in string array here or number array.
02:26 So that's, I think, because TypeScript really treats everything as an object. And so everything except for primitives as an object, really. The second solution I found, which is maybe a little bit more descriptive than this lowercase object type, is by using extend record string.
02:45 This at least gives you a little bit more description as to what's happening here, but it behaves actually the same as the object type there. So it's not entirely perfect. But what you notice here is that we get these kind of like cascading constraints throughout our application and throughout kind of our types here.
03:02 Because if we don't have, let's say, this one, then we're going to get an error. Oh, actually, no, if we don't have the other one, then we're going to get an error just up here because the thing that it relies on doesn't have the proper constraints.
03:14 So that's a pattern that you see a lot in open source libraries, especially where they have these common constraints that need to flow in through different types and things like that. But generic constraints are extremely powerful for making sure that really that your type helpers are being used in the correct way.