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 ornull
-
In Typescript, the equivalent
Option
property, represented by a?
, is defined as either a specific Type orundefined
(in general, Typescript discourages the use ofnull
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 ofundefined
. - 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 theundefined
value to thenull
value needed in the Backend type logic.
- 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
-
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 isundefined
. 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 asundefined
/null
/NaN
and treat them as equivalent.