2.7 KiB
title | date | toc | images | tags | ||
---|---|---|---|---|---|---|
Get inferred type for a generic parameter in TypeScript | 2023-01-28T14:50:54-05:00 | false |
|
Have you used Zod? 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,
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:
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:
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.