bgenc.net/content/posts/2022.01.28.typescript-get-inferred-type.md
Kaan Barmore-Genç 13fc5afc79
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Get inferred type for a generic parameter in TypeScript
2023-01-28 15:13:12 -05:00

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
dev
typescript

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.