(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
@typedefbecause 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 🙏