Typesafe checks against TypeScript interface property names at runtime
Learn how to safely use TypeScript interface property names at runtime
With TypeScript, interfaces only exist during development time. Still, sometimes, we might want to refer to interface property names at runtime. What can we do then?
In this article, I’ll share a cool and typesafe trick to be able to refer to interface property names at runtime; that is, long after the TS compiler has removed all traces of our interfaces.
TypeScript interfaces disappear
TypeScript interfaces are awesome. So awesome that they don’t even exist at runtime.
While this great for the application’s bundle weight (you can use types as much as you want without impacting the bundle size), it is sometimes annoying because we’d still like to be able to refer to some of those types. The same goes for generics, but that’s another story!
For instance, you might want to do runtime validation of JSON data to ensure that inputs do indeed correspond to your expectations.
In this article, I won’t dive into the details of “why” interfaces disappear, and instead focus on how you can perform strongly typed runtime checks validated at compile time and safe against refactoring.
A concrete use case
In my last post, I’ve discussed input validation based on something that I’ve worked on last week. While integrating input validation for my current project, I needed to handle partial updates.
Per definition, partial updates (usually sent through HTTP POST or PATCH requests) usually only contain a subset of the data that should be updated on the server side. A partial update request may contain a single field to update, or multiple ones. Of course, if a given field is present, then it needs to be properly validated.
After passing the first layer of input validation, my REST controllers dispatch CQRS commands, which are taken care of by a command handler.
That command handler receives the partial update data and must verify each field. In my case, the received partial update is based on a custom type that looks like this:
So, an update object may contain any field, depending on what T
is set to. In this case, the type is actually a PartialUpdate<MeetingDto>
. For the sake of the example let’s imagine that MeetingDto
only has the following properties: A
, B
and C
.
In pseudo-code, my goal was to do the following:
for each(property of partialUpdateObject) {
// if property A of PartialUpdate<MeetingDto> is defined
// then perform validations for A
// ...
}
Alternatively, I could’ve written tons of if checks, for each property, but I could’ve missed some; I preferred to loop over those that were present instead.
Naive implementation
The simplest expression of a solution looks like this:
for (const propertyKey of Object.keys(command.update)) {
if("A" === propertyKey) {
// perform validations for A
}
}
But if you’re like me, then you probably won’t like doing this either. Can you see the issue? To JS developers it might not matter, but to someone who likes strong typing, this is pure heresy. If the “A” property is ever refactored/renamed; then this code will break immediately and that’s bad.
So what can we do instead?
A cleaner solution
Fortunately for us, since TypeScript 2.1, we can use the keyof
keyword, also called Indexed Type Query. Using keyof
, we can get the type of permitted property names for a type T
:
type PartialUpdateKeys = keyof PartialUpdate<MeetingDto>; // "A" | "B" | "C" | ...
We’re almost there. Now the trick is to combine generics with keyof
to have strong typing for property names, while still being able to compare against those property names at runtime.
We can achieve this with the following custom type:
export const propertyOf = <SomeType>(name: keyof SomeType) => name;
In the above expression, the generic type passed when using propertyOf
is used in combination with keyof
to ensure that the value passed in is indeed a valid property name for the type. Finally, the name that is passed in is returned in the output.
By using this, we can indeed refer to known-to-be-valid property names at runtime, even if the interface is gone.
Here’s how this is used:
if(propertyOf<PartialUpdate<MeetingDto>>("A") === propertyKey) {
// do something
}
This already feels cleaner (and much safer!).
Now there’s also another variant that can make the code slightly more readable:
export const propertiesOf = <SomeType>() => <T extends keyof SomeType>(name: T): T => name;
This variant can be used as follows:
const meetingUpdateProperties = propertiesOf<PartialUpdate<MeetingDto>();
...
if(meetingUpdateProperties("A") === propertyKey) {
// ...
}
I found this technique here on StackOverflow: https://stackoverflow.com/questions/33547583/safe-way-to-extract-property-names
Conclusion
In this article, I’ve explained how you can implement safe runtime property name checks against interface property names, even though those interfaces are long gone at that time.
This serves me well and feels safe to use from a type safety point of view; so what more can we ask?
I’m curious to know if you know other/better/safer alternatives!
That's it for today! ✨