gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.84k stars 503 forks source link

Are fp-ts data types serialization-safe? #1515

Open golergka opened 3 years ago

golergka commented 3 years ago

📖 Documentation

I want to incorporate fp-ts into my current project. However, in it, I routinely pass along data types from client to server and back via JSON, and try to keep all types that I use to be transparent in that regard; I don't use classes, and I even try to use strings instead of numbers wherever possible (for example, for all id types).

Would I be able to use fp-ts types in the same way?

I haven't found the answer to this question in the docs. I have browsed the code, and, for example, Either seems to be completely serialization-safe, as far as I understand. But without confirmation in the documentation, I don't know whether all other types are, and more importantly, even if they are right now, is it part of their contract and they're intended to be this way, or it can change in future versions of the library.

May be this question is already covered in documentation and I'm just bad at reading it. However, if it is not, I'd kindly suggest to include it in the docs, and would glad to help with such a change myself.

samhh commented 3 years ago

In my experience fp-ts' sum types like Either and Option are safe to serialise, but you should be careful that after serialisation they're ephemeral, else you risk tying yourself to implementation details that could one day change.

golergka commented 3 years ago

That's exactly why I asked this question, so I can be sure that this won't change.

mlegenhausen commented 3 years ago

Yes all data types that do not describe a possible side effect or a function are serializable. They are just plain java scripts objects. Cause the upcoming release of fp-ts and io-ts do not change anything in this regard and serializability can be seen as a feature of fp-ts, this will be true for the unseeable future.

samhh commented 3 years ago

I can't speak for @gcanti but I doubt you'll get a guarantee that the type internals won't ever change.

Thinking about long-term storage, an alternative might be to serialise Option<A> as A | null and the reverse for deserialisation, and likewise Either<E, A> as E | A. This'll only behave incorrectly in the former case if your A is null and in the latter case if your E and A overlap.

If you're using io-ts it's quite trivial to define codecs like this that can handle both stages (e.g. optionFromNullable from io-ts-types), though it is additional overhead. You could alternatively assume the underlying types won't change and only start using this approach if/when they do.

kylegoetz commented 3 years ago

I do what @mlegenhausen suggests. We use io-ts to define the API contracts. You can define a codec that will validate input to ensure it conforms to the codec's definition, and it comes with a wrapper type that converts your codec into a type. I then tend to export the codec as export const GetFooResponseC = t.type({id:t.number, name: t.string, ...}) and export type GetFooResponse = t.TypeOf<typeof GetFooResponseC>.

GetFooResponse will be a type of form { id: number; name: string }

That way the rest of the API can use the codec to validate, there is a centralized place that is self-documenting for devs to know what the API will respond with, and if you are writing a full-stack application, you have access to the codec for validating incoming data to make sure conforms, and you can easily write mocks and other test code that acts as expected.