Skip to content

Proposal to add Typescript typings to toJSON and add ability to create toJSON types from models (includes a working implementation) for typing models across API calls #1708

@thesunny

Description

@thesunny

I have an Objection Model (for example Person) and am sending it from the server to the client. Before I send it, I convert the Model to JSON by calling, person.toJSON()

On the client, it would be valuable to have the proper type information for the JSON object but right now, you'd probably type it as an any type and lose all the type-checking.

As mentioned, the data that is transferred from the server to the client is not an actual instance of the Model. Instead, it is just a plain Javascript object that is returned when I call person.toJSON(). This means that it lacks the instance methods from the Model.

Currently, there does not appear to be a way to get the proper type information after calling person.toJSON().

I've created a proof of concept that can be merged into Objection with a little work but I thought I'd post an issue here first to get some feedback on whether a PR is worth it.

For those interested, here is a dump of the proof of concept code. The code below will show no typescript warnings.

import Knex from "knex"
import { Model } from "objection"

class Pet extends Model {
  name!: string
  species!: string
}

class Country extends Model {
  name!: string
}

class Person extends Model {
  name!: string
  age?: number
  country?: Country
  pets!: Pet[]
}

const takesKnex = (_: Knex) => 1
const takesPerson = (_: Person) => 1
const takesMaybePerson = (_?: Person) => 1
const takesCountry = (_: Country) => 1
const takesMaybeCountry = (_?: Country) => 1
const takesPet = (_: Pet) => 1
const takesMaybePet = (_?: Pet) => 1
const takesString = (_: string) => 1
const takesMaybeString = (_?: string) => 1
const takesNumber = (_: number) => 1
const takesMaybeNumber = (_?: number) => 1

type ItemOfArray<TArray> = TArray extends (infer TItem)[] ? TItem : never

type ArrayOfTypedJSONFrom<TArray> = Array<TypedJSONFrom<ItemOfArray<TArray>>>

type TypedJSONFrom<T> = {
  [key in keyof T]: T[key] extends Function // remove the instance methods
    ? never
    : T[key] extends Model // properties that are models
    ? TypedJSONFrom<T[key]>
    : T[key] extends Model[] // properties that are array of models
    ? ArrayOfTypedJSONFrom<T[key]>
    : T[key]
}

/**
 * Takes an instance of an Objection `Model` and converts it using `toJSON`
 * and includes all the typing information.
 *
 * @param model instance of Objection `Model`
 */
function toTypedJSON<T extends Model>(model: T): TypedJSONFrom<T> {
  return model.toJSON() as TypedJSONFrom<T>
}

/**
 * You can export the PersonJSON type and use it in the client to add typing
 * information to the JSON received by the client.
 */
type PersonJSON = TypedJSONFrom<Person>

// tests
;async () => {
  const person = await Person.query().findById(1)
  takesPerson(person)
  takesKnex(person.$knex()) // this passes
  takesString(person.name)
  takesMaybeNumber(person.age)
  const pet = person.pets[0]
  takesPet(pet)
  takesString(pet.name)
  takesString(pet.species)
  const pojo = person.toJSON()
  const json = toTypedJSON(person)
  takesString(json.name)
  takesMaybeNumber(json.age)
  // takesKnex(json.$knex()) // this will fail because the methods are removed
  const jsonCountry = json.country
  takesMaybeCountry(jsonCountry)
  const jsonPets = json.pets
  takesString(jsonPets[0].name)
  takesString(jsonPets[0].species)
  takesString(jsonPets[1].name)
  takesString(jsonPets[1].species)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions