Typescript and JSDoc, shared names for Types + Values
Tags: typescript, jsdoc, zod, javascript
Published: 2023-09-18 21:05:58

(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.

```ts
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:

```ts
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:

```ts
// 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`

```js
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:

```javascript
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:

```js
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.