Typeshare

Typeshare is a tool that supports the synchronisation of type definitions between Rust and other languages.

How we use it in PermaplanT

In our application, Typeshare is used to extract type definitions from the backend Rust code and generate corresponding TypeScript definitions for the frontend. This ensures that both the backend and frontend share the same data structures, reducing the risk of type mismatches and improving overall code consistency and reliability.

Typeshare is integrated in the build scripts for the frontend, so the corresponding types are kept up to date every time the application is built.

Important remarks:

Optional properties

One special situation with the type translation had to be handled additionally in the Frontend code:

  • In Rust, Option properties are defined as either a specific Type or null

  • In Typescript, the equivalent Option property, represented by a ?, is defined as either a specific Type or undefined (in general, Typescript discourages the use of null to represent empty values)

  • As such, the Typeshare generated types in the frontend, when containing Option properties, they get translated as a typescript optional ?.

  • In theory this is not an issue, but because of the way that our data is passed between the backend and frontend through our API, an interesting behaviour can be observed:

    • First, a Backend object with an 'empty' optional property gets serialized (using serde) into a JSON object to be passed through a request to the Frontend. In this process, the value for that empty property gets set to null, according to the type definition in Rust.
    • This object gets received by the API request in the Frontend, where it is typed automatically into the type generated by Typeshare. For an optional property, this means allowing either the specific type or undefined as allowed values.
    • But, due to Type coercion in Typescript/Javascript not really checking the value for each property of the object, in this scenario the property value ends up being null instead of undefined.
    • Similarly, a new Object of that type, if generated by the Frontend code (for example, in a form), will have the empty properties as undefined values. When sending this through a POST/PUT request to the Backend, the Rust code (with serde) will correctly map the undefined value to the null value needed in the Backend type logic.
  • In most cases in the Frontend code, this stray null does not end up being an issue, as we usually check such missing properties with a simple negation (!object.property), and in JS/TS !undefined === !null.

  • The problems arise for the scenarios where we need to compare an old value with a new one, as in JS/TS undefined !== null

  • A specific scenario where this was an issue was in Forms which include optional number properties. This was an issue, as a value loaded from the Backend, as null, would correctly show an empty input field, but that would not be equivalent with an input field that was empty (or for example was emptied by deleting the value). In this latter case, the value is undefined. As such, form validation, as well as change-tracking for that value was causing issues and had to be handled individually.

  • For this, a mapping util function was used, to make sure we would map the value of the object, when loading the form and when tracking changes, to be represented by undefined for all cases that were not a valid numerical input. This would ensure consistency between values as undefined / null / NaN and treat them as equivalent.