Let's say you build a little user management app and need a `User` type. So you define one:
interface User {
id: number;
username: string;
created: Date;
email: string;
}
If you want to build a REST API that returns that user, you can just do that: router.get('/user/:id', (id: number & Positive): User => {
//...
});
Deepkit automatically serialises the object into JSON using the type information in `User`.You want to accept a User object for creating a user in the database? No problem:
router.post('/user', (user: User): User => {
//...
});
You probably want to enrich the interface with additional constraints: interface User {
id: integer & Positive;
username: string & MinLength<3> & MaxLength<32>;
created: Date;
email: Email;
}
And with that the framework automatically validates all incoming requests against the User object and its containing validation constraints. It also deserialises it automatically from JSON for example. If the `User` is a class, it instantiates a class instance.But you don't want to require all fields, like id and created should be set by the server:
router.post('/user', (user: Omit<User, 'id' |'created'>): User => {
//...
});
This works equally well. And the best part: you get API documentation for free with the Deepkit API Console, just like you know from Swagger UI.Ok, but we need to save the User in our database, right? We do not need to duplicate the User in yet another language like with Prisma. Just re-use the User interface and annotate the fields.
interface User {
id: integer & PrimaryKey & AutoIncrement;
username: string & MinLength<3> & MaxLength<32> & Unique;
created: Date;
email: Email & Unique;
}
We still use the very same interface, but have added additional meta-data so you can use that exact same type now also for the ORM. router.get('/user/:id', async (id: number & Positive, db: Database): Promise<User> => {
return await db.query<User>().filter({id}).findOne();
});
Next, let's go to the frontend. We obviously have to request the data from our rest API. We can just do that and reuse the `User` interface once again. const userJson = await fetch('/user/2');
const user = cast<User>(userJson);
`cast` automatically validates and deserialises the received JSON. We import `User` wherever needed and this works because the meta-data is not tightly coupled to any library, so you won't pull in for example ORM code just because you use database meta-data. You can literally use TypeScript's powerful type-system to your full advantage in many small to super complex use-cases and let the framework handle all the validation, serialisation, and database stuff.And like that you are able to reuse types everywhere: From database to http router, configuration, RPC, API documentation, dependency injection, message broker, frontend, and more. That's not possible in this form in any other framework or when you combine lots of third-party libraries and glue them manually together.
Deepkit separated the functionality in libraries which would allow you to use their features in Fastify and Prisma, too, but that would mean you lose one of the biggest advantage of all: Reusing types.
> This does not work with Fastify and Prisma
The point of Prisma is that it generates types for you from the Prisma schema, right? Are the generated types not interchangeable throughout the application?
Fair point about expressiveness, but if your data must cross application boundaries then you are somewhat limited in expressiveness no matter what, because your types need to be understood by arbitrary clients.
Lack of generics is pretty annoying though. It's probably my biggest annoyance with GraphQL.
Prisma's DSL has no syntax for validation constraints, so even with openAPI generators it's incomplete.
[0] [url-redacted] [1] [url-redacted]
When you start to hear people say of their PRs “it works but I just have to fix the types”, that usually means the codebase is trying to re-use too many types. In my experience.
The beauty of TypeScript is that you can have two different very narrowly specified types and the compiler will tell you if they’re compatible or not.
Reusing types is throwing away the most valuable feature of TypeScript.
For me, the type signature is part of the function signature and therefore should not be re-used.
Imagine what a disaster it would be if function signatures were reusable:
sig UserDatabase(user, db)
create: UserDatabase =>
db.write(“users”, user)
update: UserDatabase =>
db.get(“users”, user.id).update(user)
It’s too much. DRY can be taken too far.While it does sound quite great I have to try it in practise to understand would it work for me. To prevent framework fatigue I really try not to switch frameworks too often which is why I'd like to be able to reuse what I know already. And your scope sounds really big. Yet I always have liked how TS/JS community keep pushing the envelope constantly!
Wait, how can Deepkit's `cast` work on the frontend? Do you compile to WASM?
Quickly skimming the current Deepkit homepage and intro blog post didn't seem to hint at this.
My summary is that the DeepKit compiler is a JS transpiler (used as a ts-node or webpack plugin, or conveniently/invasively as a replacement of node_modules/typescript) that produces code like this:
//TypeScript
export type TypeA = string[];
//generated JavaScript
export const __ΩtypeA = ['&F'];
//TypeScript
function log(message: string): void {}
//generated JavaScript
function log(message) {}
log.__type = ['message', 'log', 'P&2!$/"'];
Where '&F' and 'P&2!$/"' are the compact bytecode translations of the types ("&" means string, "F" means array) and '__Ω' is a prefix used to avoid naming conflicts.Personally, I'd love to see a live example of code in & compiled code out on the homepage. I'm sure it's on your roadmap :)