TypeScript
TypeScript Utility Types You Need to Know
Are you ever building something in TypeScript and realize...
AGH! This package is not exporting a type I need!
Fortunately, TypeScript gives us a number of utility types that can solve this common problem.
For instance, to grab the type returned from a function, we can use the ReturnType utility:
But we have one little problem. getContent is an async function that returns a promise, so currently our Content type is actuallyPromise, which is not what we want.
For that, we can use the Awaited type to unwrap the promise and get the type of what the promise resolves to:
Now we have exactly the type we needed, even though it is not explicitly exported. Well, that’s a relief.
But what if we need argument types for that function?
For instance, getContent takes an optional argument called ContentKind that is a union of strings. I really don’t want to have to type this out manually, so let’s use the Parameters utility type to extract its parameters:
Parameters gives you a tuple of the argument types, and you can pull out a specific parameter type by index like so:
But we have one last issue. Because this is an optional argument, our ContentKind type right now is actually ContentKind | undefined, which is not what we want.
For this, we can use the NonNullable utility type, to exclude any null or undefined values from a union type.
Now our ContentKind type perfectly matches the ContentKind in this package that was not being exported, and we can use it in our processContent function like so:
Utility Types with React
Utility types can also help us a lot with our React components.
For instance, below I have a simplistic component to edit calendar events, where we maintain an event object in state and modify the event title on change.
Can you spot the state bug in this code?
Doh, we are mutating the event object directly.
This will cause our input to not work as expected because React will not be aware of the state change and subsequently not rerender.
What we need to be doing is calling setEvent with a new object.
But wait, why didn’t TypeScript catch that?
Well, technically you can mutate objects with useState. You just basically never should. We can improve our type safety here by using the Readonly utility type, to enforce that we should not be mutating any properties on this object:
Now our prior bug will be caught for us automatically, woo!
Now, when we update our code to copy the event as needed, TypeScript is happy again:
But, there is still a problem with this. Readonly only applies to top level properties of the object. We can still mutate nested properties and arrays without errors:
But, now that we are aware of Readonly, we can combine that with its sibling ArrayReadonly, and a little bit of magic, and make our own DeepReadonly type like so:
Thanks to Dean Merchant for the above code snippet.
Now, using DeepReadonly, we cannot mutate anything in the entire tree, preventing a whole range of bugs that could occur.
Which will only pass type check if treated properly immutably:
One additional pattern you may want to use for this kind of complexity is to move this logic to a custom hook, which we can do like so:
This allows us to simply provide the properties that have changed and the copying can be managed automatically for a nice DX and safety guarantees.
But we have a new problem. updateEvent expects the full event object, but what we intend is to only have a partial object, so we get the following error:
Fortunately, this is easily solved with the Partial utility type, which makes all properties optional:
Alongside Partial, it’s worth also knowing the Required utility type, which does the opposite - takes any optional properties on an object and makes them all required.
Or, if we only want certain keys to be allowed to be included in our updateEvent function, we could use the Pick utility type to specify the allowed keys with a union:
Or similarly, we can use Omit to omit specified keys:
Moar utilities
We touched on a good bit of typescript utilities here! But here is a quick overview of the remaining ones, which are all pretty useful in my opinion.
Record<KeyType, ValueType>
Easy way to create a type representing an object with arbitrary keys that have a value of a given type:
Exclude<UnionType, ExcludedMembers>
Removes all members from a union that are assignable to the ExcludeMembers type.
Extract<Union, Type>
Removes all members from a union that are not assignable to Type.
ConstructorParameters<Type>
Just like Parameters, but for constructors:
InstanceType<Type>
Gives you the instance type of a constructor.
ThisParameterType<Type>
Gives you the type of the this parameter for a function, or unknown if none is provided.
OmitThisParameter<Type>
Removes the this parameter from a function type.
Conclusion
Utility types in TypesScript are useful. Use them.
About me
Hi! I'm Steve, CEO of Builder.io.
We make a way to drag + drop with your components to create pages and other CMS content on your site or app, visually.
You may find it interesting or useful: