Get inferred type for a generic parameter in TypeScript
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Kaan Barmore-Genç 2023-01-28 15:13:12 -05:00
parent 90ec401ea1
commit 13fc5afc79
Signed by: kaan
GPG key ID: B2E280771CD62FCF

View file

@ -0,0 +1,79 @@
---
title: "Get inferred type for a generic parameter in TypeScript"
date: 2023-01-28T14:50:54-05:00
toc: false
images:
tags:
- dev
- typescript
---
Have you used [Zod](https://zod.dev/)? It's a very cool TypeScript library for
schema validation. Compared to alternatives like Joi, one of the biggest
strenghts of Zod is that it can do type inference. For example,
```ts
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
});
type Person = z.infer<typeof PersonSchema>;
// This is equivalent to { name: string; age: number; }
```
Now I was recently working on a database client, where I'm using a validator
function to ensure the data on the database matches what the client expects. I
then take advantage of TypeScript's type inference so the type of everything
matches. It looks like this:
```ts
class Database<T> {
constructor(validator: (input: unknown) => T);
function get(key: string): T | undefined { /* ... */ }
function put(key: string, data: T) { /* ... */ }
}
// Note I didn't have to specify the type parameter,
// TypeScript infers it from the validator argument
const PersonDB = new Database(PersonSchema.parse);
```
At this point I started to wonder, could I do something similar to what Zod does
and get the inferred type for the objects that are stored in the database? While
this is not required in this example above since I could get the type from Zod,
the validator function doesn't necessarily have to be implemented with Zod.
After reading through Zod's codebase, I found the trick they use, and it's very
simple. Let's see it:
```ts
class Database<T> {
readonly _output!: T;
constructor(validator: (input: unknown) => T);
function get(key: string): T | undefined { /* ... */ }
function put(key: string, data: T) { /* ... */ }
}
type EntityOf<D extends Database<any>> = D["_output"];
const PersonDB = new Database(PersonSchema.parse);
type Person = EntityOf<typeof PersonDB>;
```
This is surprisingly simple. We add a property `_output` to the class, which has
the inferred type. We can then get the type through that property with
`D["_output"]`. The `!` in the definition of the property is there because we
never actually set any value for `_output`. TypeScript normally will detect and
warn us that we did not set `_output`, but the exclamation point suppresses
that.
This is not without drawbacks of course, because the `_output` property will be
visible in the instances of the class. We can't hide the property with `private`
because TypeScript won't let us look it up in `EntityOf` if we do so. So the
best we can do is document the fact that this should not be used, and throw in
the prefix so it stands out from regular properties.