Closed denis-ilchishin closed 2 months ago
@denis-ilchishin Hi,
I may look into supporting multiple registries into the future; unfortunately this wouldn't be feasible in the short to medium term, but will be exploring additional options down the road.
I've been trying to build a custom type system on top of typebox, and for my usecase, it is essential to have different validation logic for server and client (browsers in particular), while sharing the same code base (basically custom server/client contract type system).
It's not really documented anywhere, but thought I'd share how I usually setup projects for shared types, then maybe offer a suggestion for custom registration specific to clients and services. Discussing project structuring is usually quite difficult, so here is some ascii art + tsconfig.json for reference. The following is usually how I structure things for IO contracts with cross dependent shared types.
// ┌──────────────────────────┐
// ┌──┤ @app/api/foo/service │
// ┌──────────────────────┐ │ └──────────────────────────┘
// ┌──┤ @app/api/foo/types ├─┤
// │ └──────────────────────┘ │ ┌──────────────────────────┐
// │ └──┤ @app/api/foo/client │
// ┌────────────────┐ │ └──────────────────────────┘
// │ @app/types ├─┤
// └────────────────┘ │ ┌──────────────────────────┐
// | │ ┌──┤ @app/api/bar/service │
// | │ ┌──────────────────────┐ │ └──────────────────────────┘
// | └──┤ @app/api/bar/types ├─┤
// | └──────────────────────┘ │ ┌──────────────────────────┐
// | | └──┤ @app/api/bar/client │
// | | └──────────────────────────┘
// | | |
// | | |
// define and register application | |
// wide types here | |
// | |
// register api level types here which |
// may import and embed app |
// level types |
// service and client import @api/types
// through @app/api/x/types
// ./tsconfig.json
{
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
// application wide types live here.
"@app/types": ["libs/types/index.ts"],
// foo api types
"@app/api/foo/types": ["libs/api/foo/types/index.ts"],
"@app/api/foo/client": ["libs/api/foo/client/index.ts"],
"@app/api/foo/service": ["libs/api/foo/service/index.ts"],
// bar api types
"@app/api/bar/types": ["libs/api/bar/types/index.ts"],
"@app/api/bar/client": ["libs/api/bar/client/index.ts"],
"@app/api/bar/service": ["libs/api/bar/service/index.ts"],
}
}
}
So, the above is tends to be quite flexible because you have a centralized location to define and register application wide types (things like Users, Roles, Identity, etc), as well as a places to define service level contracts down stream (things like CreateUserRequest which may embed the User type from the app level).
In terms of type and format registration, I would normally define everything inside @app/types
which forces the logic there to be extremely generic and specific to type / structural validation only. However you can actually defer registration all the way down to the client
and service
level if you need.
// ┌──────────────────────────┐
// ┌──┤ @app/api/bar/service │ <-- define service specific registration logic here
// ┌──────────────────────┐ │ └──────────────────────────┘
// │ @app/api/bar/types ├─┤
// └──────────────────────┘ │ ┌──────────────────────────┐
// | └──┤ @app/api/bar/client │ <-- define client specific registration logic here
// | └──────────────────────────┘
// |
// |
// only define types here
// (no registration)
The reason this should work is because TypeBox types and registries are fully decoupled, so it is possible to define the types/schematics in one place, and the validation / registration logic in another. By deferring the registration logic all the way down to the service
and client
level, application code that imports either of these will over get registrations for module they import (meaning client code gets no server code)
Will close of this issue for now as there isn't anything to action here in the short term. Hopefully the diagrams and write up above helps you out structuring TB types in your project. I should provision though that the above is very conducive to mono repositories setups, so can recommend things like Nx, Hammer (mine) or similar as this can really help getting cross dependencies working correctly (Nx has some very good tooling to generate graph like dependencies like the above to help you learn what code is being referenced where)
Cheers! S
Thank you for your work!
Ref: #442
I've been trying to build a custom type system on top of typebox, and for my usecase, it is essential to have different validation logic for server and client (browsers in particular), while sharing the same code base (basically custom server/client contract type system). And obviously I don't want to include server-only code to client's bundle. Right now the only way to do that, is to import schema and validation logic separately, something like:
This way I can reuse the same code from
package/type
, but it would be so much better, if I can provide separate instances of these registries and completely isolate usage of typebox, so I don't have to worry about other libraries register a type or format with the same name, or something like that.