(written by a human, no AI content)
I saw this pattern recently, taking advantage of the fact that Typescript doesn't mind if you re-use the same name for a value and a type.
import * as z from "zod"
// Define a schema
export const User = z.object({
name: z.string()
})
// export an inferred type for use elsewhere
// but, using the same name, `User` 🤯
export type User = z.infer<typeof User>
So we have...
export const User ...
export type User...
... it might seem odd, but Typescript is more than happy with this - after all, types and values are distinct! So, providing that you're happy with any potential confusion in the future , it's a pattern you can use right away!
Why would you though? Well, it might make more sense in the context of a consumer - consider the following in a separate module:
import { User } from "./zod"
function signIn(user: User) {
console.log(user);
}
Here we have a single import: User
. We know the module exports both a type and a value with that name - but when seen in type parameter position like this, Typescript will just use the type User
without issue. A naming 'collision' just doesn't occur.
In that very same file, we could also do:
// same file, different context
const parsed = User.safeParse({ name: "Shane" })
which is nice - because now we're just in regular JavaScript territory here, using the value called User
. Typescript understands that we're not in a type position, so everything 'just works' and we didn't need to think of a separate name for it, like UserSchema
or similar. 🥰
JSDoc
All of the examples above were in Typescript - but if you're using JSDoc this works almost identically.
First, instead of exporting both of these...
export const User ...
export type User ...
we'll need to replace the second one - since export type ...
is only supported in Typescript.
To do this, albeit in a less-than-ideal way, we can use a @typedef
import * as z from "zod"
export const User = z.object({
name: z.string()
})
/**
* @typedef {import("zod").infer<typeof User>} User
*/
Using infer
from Zod, we're achieving the exact same inference as we did in Typescript - it's just that it's inside a comment this time instead!🥹.
@typedef
isn't ideal for all situations (for another time...), but in this case it has exactly the effect we're looking for.
Considering a consumer in JS, as we did with the Typescript example, it would look like this:
import { User } from "./jsdoc-zod";
/**
* @param {User} user
*/
function signIn(user) {
console.log(user);
}
Again, note we're only importing User
- which is both a type and a value in the other module. Typescript still has no issue understanding that within a @param
block, we are referring to the type and not the value.
That means the previous "in the same file" example works just as it did before, presented here as a single snippet:
import { User } from "./jsdoc-zod";
/**
* @param {User} user - works as expected in type position 🥰
*/
function signIn(user) {
console.log(user);
}
// works as expected as a value too 🥰
const parsed = User.safeParse({ name: "shane" })
Conclusion
It's becoming table-stakes for any schema parsing library like Zod to support rich type inference. Having types derived from runtime validation code is a powerful pattern, one that I can see growing in popularity.
This post highlights how you can use a single name for a value and a type - and how it can be done in JSDoc along with Typescript.
Top comments (2)
Personally, I don't like
@typedef
because they increase the bloat of JSDoc comments, but exporting Type and helper with the same name definitely looks convenient.Yep I’m not a big fan of it either - but nice to to know it works if you don’t have the choice 🙏