Improving Typescript experience in Rails by generating a Typescript schema from Rails models (with enums and associations)

15th September 2021 – 518 words

For a side project (which I won’t reveal at the moment), I develop most of the frontend in Vue/TSX+Typescript. At the moment, I don’t have a specific “API”, but just use Inertia.js with a couple of main models that are simply serialized by model.as_json. Having then to type out a deep model schema with dozens of connected models (using delegated types in my case) by hand is a PITA. Fortunately, I came across schema2type, which is a Rails plugin that generates a Typescript schema for all of your models by only using schema.rb, BUT because of this constraint, it cannot know about Rails enums and associations.

This is why I quickly cobbled together a script that improves on that idea:

Gist

To “install” and run:

wget https://gist.github.com/zealot128/419949f1c426330493c84bb8eadc4533/raw/1c4629c05506630d3fe8543320cd6d2026405d8e/rails-models-to-typescript-schema.rb

rails runner rails-models-to-typescript-schema.rb > app/javascript/types/schema.d.ts

Afterwards you can easily use schema.MyModel.<TAB> to autocomplete all the things! In my usecase, I then built upon that schema to define more specific types that I serialize and pass to my frontend (via Inertia.js). It also works for the built-in ActiveStorage models and spit out this:

declare namespace schema {

  interface ActiveStorageBlob {
    id: number;
    key: string;
    filename: string;
    content_type: string | null;
    metadata: string | null;
    service_name: string;
    byte_size: number;
    checksum: string;
    created_at: string;
    variant_records?: ActiveStorageVariantRecord[];
    attachments?: ActiveStorageAttachment[];
    preview_image_attachment?: ActiveStorageAttachment;
    preview_image_blob?: ActiveStorageBlob;
  }

  interface ActiveStorageVariantRecord {
    id: number;
    blob_id: number;
    variation_digest: string;
    image_attachment?: ActiveStorageAttachment;
    image_blob?: ActiveStorageBlob;
    blob?: ActiveStorageBlob;
  }

  interface ActiveStorageAttachment {
    id: number;
    name: string;
    record_type: string;
    record_id: number;
    blob_id: number;
    created_at: string;
    blob?: ActiveStorageBlob;
  }
}

The 80 line script already supports:

  • belongs_to, has_one and has_many associations (un-polymorphic)
  • enums
  • null and not null
  • type mapping, borrowed from the original schema2type script

Security implications: The script generates a type with all attributes, that includes password-digests, tokens etc. You probably shouldn’t send those informations to frontend clients in general. For the typings that should be fine though, because, typings are only used in development and discarded in a production build.

To further improve your Typescript experience, have a look at https://github.com/bitjourney/ts_routes-rails - IMO here you really should make sure to not leak Routes to clients that you don’t want them to access. Depending on your production build unused route helpers may or may not be removed due to treeshaking.