Ultra-minimal JSON schemas with TypeScript inference
github.com
github.com
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.
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.
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.
Repo: https://github.com/justjake/ts-simple-type
Compiler input and output: https://github.com/justjake/ts-simple-type/blob/jake--compil...
Thrift compiler: https://github.com/justjake/ts-simple-type/blob/jake--compil...
Python compiler: https://github.com/justjake/ts-simple-type/blob/jake--compil...
I use @sinclair/typebox and the point is that I can get a JSON Schema out the other side, usable in-process and as a communicative description of what my code expects, just by evaluating my code and printing out an object.
It does support string literal types, FWIW. I'm good with it not supporting generics, because the places where I need an interchange format generally don't. Mapped types would be nice but aren't really critical to where I need to generate interchange formats; dependent types that result from transforming these can use mapped types, so it's close enough.
For my money, typebox has far-and-away the best user experience of any of the flavor of libraries you described, and IMO is probably worth studying.
It does come with some costs. First, the type inference using these large mapped types (at least in our implementation and/or scale) noticeably slows down our Typescript type checking; the public api files with our runtime type declarations always top our build profiling. Second, it’s annoying to need to re-implement an existing type as a runtime-type if you start to need it in an API specification. There’s a tension in the codebase between using the easily-available, language native type system, or making do with the runtime type stuff which increases verbosity/noise and decreases expressiveness.
We have thousands of lines of types, including many types inferred from `as const` literals, written in Typescript. Rewriting those types in another schema language, such as typebox or Protobuf, would take a bunch of time and spread that tension from an isolated spot (backend API handlers) into every part of the codebase. It imposes new constraints, and requires devs to learn more things.
The libraries I listed and the compiler I’m building try to address this issue by supporting the native typescript declaration syntax. No more red types and blue types, much better gradual adoption pathway, and ideally one less thingy to learn for most developers.
> This also needs a CLI.
I’m not planning on implementing one at the moment. The project goal is to greatly expand the use-cases for Typescript types by making it much easier to build custom compile targets.
A CLI means directing much more of my energy to configuration logic and file system IO. As a library, it’s clear how users should “configure” behavior - write or subclass a compiler target and implement whatever you want!
But on the client I would use a JSON-schema validator anyways, otherwise I'd need something like "json-schema-to-zod"? Seems like a roundabout way to address my problem
It reimplements the type system as… a stack based VM that expands the TS typedefs if needed at runtime?
I would use it if the risk factor wasn’t so high. I’m too scared of needing to maintain their crazy cool stuff to use it.
I would love Microsoft to build those APIs first party and officially support them.
EDIT:
> I know there's a TypeScript goal that TypeScript types should not impact runtime behaviour
This is why I’m rolling up my sleeves to build my own typescript-to-X tooling - it seems like no one is gonna do it for me, just the way I like — so I should make it easy for everyone to do it the way they like.
It just needs to have nice macro system.
It would solve this problem and others like ie. pattern matching.
It also doesn't have any typescript awareness which is required to build this kind of functionality - you want to have static type introspection available in macros so you can generate code based on provided types.
[0] https://github.com/sweet-js/sweet-core/graphs/contributors
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]) { ... }
}
}
The actual implementation of course had to take care of implicit nulls and other caveats of the TypeScript type system.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?
I thought maybe Quicktype (https://quicktype.io/) was that, but it looks like it only takes JSON as input, not TS types.
It also works the other way, you can define a zod schema and you can infer types for your data from it.
From the documentation:
const T = Type.String() // const T = { type: 'string' }
type T = Static<typeof T> // type T = string
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].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.
And if you're doing code generation, there is also json-schema-to-typescript
I get the legibility (and IntelliSense) of typescript interfaces but can make use of the all the validation libraries that use JSON Schema.
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.
Think bigger.
Then the meta solution will become self-evident.
Convenient in some ways (e.g. if you are only comfortable in JS), and does nothing useful in other contexts.
Nothing inherently wrong with it, other than it seems like a missed opportunity.
Is this more than a subset of Wordnik's Swagger?
That's hardly a relevant point or meaningful comparison. The discussion is about schema validators for TypeScript/JavaScript.
https://swagger.io/docs/specification/data-models/keywords/
This is a easier write, easier to read, compatible with JSON Schema alternative. Spartan Schema could be used instead of JSON Schema to make the document easier for humans to work with.
> Then the meta solution will become self-evident.
> Think bigger.
Are you going to share your big idea?
You seem to be jumping from topic to topic without being able to express your thoughts very well.