After some frustration with the TypeScript schema library ecosystem, I've decided that I'd prefer to declare my types using TypeScript's excellent type syntax, so I can take advantage of generics, mapped types, etc. Then, I'll take those TypeScript types and compile them to whatever alternative schema format I need.<p>There are many libraries that claim to convert your Typescript types to other formats, such as ts-json-schema-generator, ts-to-zod, or typeconv/core-types-ts. These libraries work by interpreting the Typescript AST, essentially re-implementing a bare-bones type system from scratch. Most do not support advanced Typescript features like generic application, mapped types, or string literal types. So, what's the point? To avoid those limitations, I use Typescript's first-party ts.TypeChecker API to analyze types, and an existing library called ts-simple-type (which I've forked) to convert from ts.Type to a more usable intermediate format. Then, I recurse over the intermediate format and emit "AST nodes". It's pretty simple, but seems promising.<p>So far, I have compilers from TypeScript type to Python 3 and Thrift. But I plan to add OpenAPI/JSONSchema, Protobuf (Proto3), Kotlin, Swift, and maybe Zod and Avro. Each target language is around ~300 LoC so they're pretty easy to put together.<p>My ultimate goal is to use this toolkit with Notion's internal types, which are quite a bit more complex than something like ts-to-zod can handle.<p>Repo: <a href="https://github.com/justjake/ts-simple-type" rel="nofollow">https://github.com/justjake/ts-simple-type</a><p>Compiler input and output: <a href="https://github.com/justjake/ts-simple-type/blob/jake--compilers-as-features/test/snapshots/compiler.spec.ts.md" rel="nofollow">https://github.com/justjake/ts-simple-type/blob/jake--compil...</a><p>Thrift compiler: <a href="https://github.com/justjake/ts-simple-type/blob/jake--compilers-as-features/src/compile-to/thrift.ts" rel="nofollow">https://github.com/justjake/ts-simple-type/blob/jake--compil...</a><p>Python compiler: <a href="https://github.com/justjake/ts-simple-type/blob/jake--compilers-as-features/src/compile-to/python3.ts" rel="nofollow">https://github.com/justjake/ts-simple-type/blob/jake--compil...</a>
I don’t know. Zod feels more reasonable path forward to accomplish the same thing (granted it’s not using the language of JSON schema if you have a dependency on that for some reason)
I wish there would be an official TypeScript solution for this. I know there's a TypeScript goal that TypeScript types should not impact runtime behaviour, but I think converting arbitrary JSON from a file or network to a typed TypeScript object is such a common use case it needs a standardised solution. There's tens of libraries that try to solve this now in different ways, all working around this area where TypeScript doesn't try to help.
I made a similar thing back in time, unfortunately not a public code though. The idea itself is simple, it was approximately something like this:<p><pre><code> const kind = Symbol();
type Spec = "unknown" | "string" | "number" | ... |
{ [kind]: "optional"; spec: Spec } |
{ [kind]: "array"; element: Spec } |
{ [P in string | number | symbol]: Spec } | ...;
function optional<S extends Spec>(spec: S): { [kind]: "optional"; spec: S } { ... }
function arrayOf<E extends Spec>(element: E): { [kind]: "array"; element: E } { ... }
// and so on, you get the idea. these helper functions exist so that
// you can write `optional(...)` or `arrayOf(...)` instead of the full specification
type Validated<S> =
S extends "unknown" ? unknown :
...
S extends { [kind]: "array"; element: infer E } ? Array<Validated<E>> :
S extends { [P in string | number | symbol]: Spec } ? { [P in keyof S]: Validated<S[P]> } :
...;
function validate<S extends Spec>(value: unknown, spec: S): asserts value is Validated<S> {
if (typeof spec === "string") {
switch (spec) { ... }
} else {
switch (spec[kind]) { ... }
}
}
</code></pre>
The actual implementation of course had to take care of implicit nulls and other caveats of the TypeScript type system.
This looks really nice as an alternative to JSON schema, which is horribly verbose. Like using RELAX NG compact syntax for XML schemas back in the day, versus horrible verbose DTDs or XSDs.<p>But I think the most natural way to express JSON schemas would be TypeScript type declarations. Is there a project that can take a TS type and generate a runtime parser/validator for it?<p>I thought maybe Quicktype (<a href="https://quicktype.io/" rel="nofollow">https://quicktype.io/</a>) was that, but it looks like it only takes JSON as input, not TS types.
We're using TypeBox [0]: you use TypeScript to create JSON Schema's and it gives you a compile-time type.<p>From the documentation:<p><pre><code> const T = Type.String() // const T = { type: 'string' }
type T = Static<typeof T> // type T = string
</code></pre>
The main thing we're using it for now, is defining an OpenAPI schema and using the types in the front-end and backend, and using the schema to validate requests and responses, both client-side as well as server-side. Validation is done using avj [1].<p>We used Joi before and want to replace that code with JSON Schema, but that is quite the hassle. I like that TypeBox uses JSON Schema underneath, so migrating to another library should be easy.<p>[0] <a href="https://github.com/sinclairzx81/typebox" rel="nofollow">https://github.com/sinclairzx81/typebox</a><p>[1] <a href="https://ajv.js.org/" rel="nofollow">https://ajv.js.org/</a>
For those wanting to infer Typescript types from JSONSchema itself there is:
<a href="https://github.com/lbguilherme/as-typed" rel="nofollow">https://github.com/lbguilherme/as-typed</a>
I recently used a package that converts typescript to JSON Schema, which I thought was pretty nice.<p>I get the legibility (and IntelliSense) of typescript interfaces but can make use of the all the validation libraries that use JSON Schema.
Even more minimal one I made years ago:<p><a href="https://wryun.github.io/yajsonschema/" rel="nofollow">https://wryun.github.io/yajsonschema/</a>
Unfortunately excessively verbose and non-portable to backends other than Node.js.<p>This is a good start, and what problems does this solve IRL? As in, actual problems that happen? (Not just theoretically possible classes of problems.) I get the narrowest use-case, and then beyond that this seems otherwise doomed to become irrelevant over time rather than gain momentum and become something meaningful in any lasting sense.<p>Think bigger.<p>Then the meta solution will become self-evident.