@slonik/typegen
A library that uses slonik to generate typescript interfaces based on your sql queries.
The idea
This library gives you type-safety of an ORM, and the flexibility of sql. Read @gajus's excellent blog post on why it's a good idea to use sql rather than ORMs or query-builders: Stop using Knex.js.
The library will make sure that return values from all your SQL queries have strict, accurate TypeScript interfaces.
It provides gradual ramp to type safety, so you don't have to spend any time manually syncing interfaces. Before the types have been generated, results will be typed as a generic dictionary - meaning you can write your queries and business logic as quickly and easily as before, but the compiler will tell you if you got something wrong once you try it out.
This method gives you strong typing, like in an ORM, but avoids the inner-platform effect that tends to come with them. You can rename columns, do any kinds of join you want, and the types generated will be based on the query, not the table, so you won't be limited by ORM feature-sets.
Simple select statements, joins, and updates/inserts using returning
are all supported - any sql query that returns a tabular value will have an interface generated for the row type. The interface will be automatically applied to the appropriate query result.
Contents
Installation
npm install @slonik/typegennpx slonik-typegen src/generated/db # initialises a placeholder directory for generated types
Usage
Setup slonik in with type generation in typescript (e.g. in a src/db.ts
file):
When you first write this code, getPeople
will have return type Promise<QueryResultType<any>>
.
But once you run getPeople()
(say, by normal usage of your application, or in an integration test), @slonik/typegen
will inspect the field types of the query result, and generate a typescript interface for it.
Afterwards, without having to modify any code, getPeople
will have return type Promise<QueryResult<Person>>
, where Person
is defined based on the query. In this case is the schema of the person
table. This allows you to get started fast - just write a query, and you will be able to use the results.
The code generation is opt-in. If you don't provide a path for the generated code to be written, you'll get a normal slonik client with no code generation hooks. It is strongly recommended you don't generate code in production (the example above relies on a simple check of the NODE_ENV
environment variable). When a falsy value is supplied to writeTypes
, sql.Person
becomes slonik's sql
template function.
Configuration
property | description | default value |
---|---|---|
writeTypes |
location to write generated typescript. if this is undefined (or falsy) no types will be written. | false |
knownTypes |
object produced by the library's codegen. By referencing the export produced in the location set by writeTypes , types will gradually improve without adding any code per query. |
undefined |
knownTypes.defaultType |
by setting this, you can force any unknown/new queries to be given a particular type. For example, in development, you may want a new query to be typed as any while you're writing code. |
undefined |
typeMapper |
custom mapper from postgres type name to typescript type as a string, as well as a corresponding function mapping the runtime values - see examples using Date and a custom enum in the tests. |
undefined |
reset |
if true, generated code directory will be wiped and reinitialised on startup. | false |
Examples
The tests are a good starting point to see a few different configurations.
How it works
The function setupTypeGen
returns a special sql
proxy object, which allows accessing any string key. When you access a key, it's considered as a query identifier. You'll get a wrapped version of slonik's sql
tagged template variable. When you use it with a tagged template, @slonik/typegen will remember your query and store the identifier against it. setupTypeGen
also returns a slonik interceptor, defining a hook to be run after every query is executed. The hook looks up the query identifier from the query taht was run, and generates a typescript interface from the field metadata provided by postgres. The interface is written to disk, and added to the KnownTypes
definition. The typescript compiler will then automatically gain type information corresponding to that query.
Recommendations
- You can re-use the same type name for many queries. But you should only do this if the types represented by any single name are the same, since the resultant type will be a union of all of the outputs (meaning
A | B
- i.e. only fields common to both will be accessible). - Check in the types to source control. They're generated code, but it makes it much easier to track what was happened when a query was update, and see those changes over time.
- After running CI, it's worth making sure that there are no working copy changes. For git, you can use check-clean:
yarn test:integrationnpx check-clean