From 13fc5afc7976d5b66d1b96947034320ea65d8797 Mon Sep 17 00:00:00 2001 From: Kaan Barmore-Genc Date: Sat, 28 Jan 2023 15:13:12 -0500 Subject: [PATCH] Get inferred type for a generic parameter in TypeScript --- ...2022.01.28.typescript-get-inferred-type.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 content/posts/2022.01.28.typescript-get-inferred-type.md diff --git a/content/posts/2022.01.28.typescript-get-inferred-type.md b/content/posts/2022.01.28.typescript-get-inferred-type.md new file mode 100644 index 0000000..97fcf3c --- /dev/null +++ b/content/posts/2022.01.28.typescript-get-inferred-type.md @@ -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; +// 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 { + 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 { + readonly _output!: T; + + constructor(validator: (input: unknown) => T); + + function get(key: string): T | undefined { /* ... */ } + function put(key: string, data: T) { /* ... */ } +} + +type EntityOf> = D["_output"]; + +const PersonDB = new Database(PersonSchema.parse); + +type Person = EntityOf; +``` + +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.