Remix.run Logo
VanCoding 5 hours ago

A big step in the right direction, but I still don't like the API, here's why: Especially in JavaScript where I often share a lot of code between the client and the server and therefore also transfer data between them, I like to strictly separate data from logic. What i mean by this is that all my data is plain JSON and no class instances or objects that have function properties, so that I can serialize/deserialize it easily.

This is not the case for Temporal objects. Also, the temporal objects have functions on them, which, granted, makes it convenient to use, but a pain to pass it over the wire.

I'd clearly prefer a set of pure functions, into which I can pass data-only temporal objects, quite a bit like date-fns did it.

jayflux 3 hours ago | parent | next [-]

This was an intentional design decision. We wanted to make sure all the temporal types could be serialize/deserializable, but as you mentioned, you couldn't implicitly go back to the object you started with as JSON.parse doesn't support that.

Instead the onus is on the developer to re-create the correct object they need on the other side. I don't believe this is problematic because if you know you're sending a Date, DateTime, MonthDay, YearMonth type from one side, then you know what type to rebuild from the ISO string on the other. Having it be automatic could be an issue if you receive unexpected values and are now dealing with the wrong types.

There is an example here in the docs of a reviver being used for Temporal.Instant https://tc39.es/proposal-temporal/docs/instant.html#toJSON

perfmode 5 hours ago | parent | prev | next [-]

This is a real pain point and I run into the same tension in systems where data crosses serialization boundaries constantly. The prototype-stripping problem you're describing with JSON.parse/stringify is a specific case of a more general issue: rich domain objects don't survive wire transfer without a reconstitution step.

That said, I think the Temporal team made the right call here. Date-time logic is one of those domains where the "bag of data plus free functions" approach leads to subtle bugs because callers forget to pass the right context (calendar system, timezone) to the right function. Binding the operations to the object means the type system can enforce that a PlainDate never accidentally gets treated as a ZonedDateTime. date-fns is great but it can't give you that.

The serialization issue is solvable at the boundary. If you're using tRPC or similar, a thin transform layer that calls Temporal.Whatever.from() on the way in and .toString() on the way out is pretty minimal overhead. Same pattern people use with Decimal types or any value object that doesn't roundtrip through JSON natively. Annoying, sure, but the alternative is giving up the type safety that makes the API worth having in the first place.

TimTheTinker an hour ago | parent | next [-]

Sounds like we need an extended JSON with the express intent of conveying common extended values and rich objects: DateTime instants (with calendar system & timezone), Decimal, BigInt, etc.

sheept 38 minutes ago | parent [-]

I disagree: this is not unlike including the schema in the JSON itself. This should be handled by the apps themselves, since they would have to know what the keys mean regardless.

If you do want the interchange format to be the one deserializing into specific runtime data structures, use YAML. YAML's tag syntax allows you to run arbitrary code inside YAML, which can be used for what you want.

VanCoding 4 hours ago | parent | prev [-]

It's not that much about type safety. Since TypeScript uses duck typing, a DateTime could not be used as a ZonedDateTime because it'd lack the "timezone" property. The other way around, though, it would work. But I wouldn't even mind that, honestly.

The real drawback of the functional approach is UX, because it's harder to code and you don't get nice auto-complete.

But I'd easily pay that price.

TimTheTinker an hour ago | parent | prev | next [-]

Updating JSON.parse() to automatically create Temporal objects (from what shape of JSON value?) without a custom reviver would be a step too far, in my opinion.

This is effectively no different from Date:

  serialize: date.toJSON()
  deserialize: new Date(jsonDate)
in Temporal:

  serialize: instant.toJSON()
  deserialize: Temporal.Instant.from(jsonDate)
causal 4 hours ago | parent | prev | next [-]

I'm with you on this. I worked on a big Temporal project briefly and I was really turned off by how much of the codebase was just rote mapping properties from one layer to the next.

qcoret 5 hours ago | parent | prev | next [-]

All Temporal objects are easily (de)serializable, though. `.toString` and `Temporal.from` work great.

VanCoding 5 hours ago | parent [-]

That's not what I mean. Even though it is serializable, it's still not the same when you serialize/deserialize it.

For example `JSON.parse(JSON.stringify(Temporal.PlainYearMonth.from({year:2026,month:1}))).subtract({ years: 1})` won't work, because it misses the prototype and is no longer an instance of Temporal.PlainYearMonth.

This is problematic if you use tRPC for example.

flyingmeteor 4 hours ago | parent | next [-]

You would need to use the `reviver` parameter of `JSON.parse()` to revive your date strings to Temporal objects. As others have said, it's a simple `Temporal.from()`

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Bratmon 3 hours ago | parent | next [-]

Having to provide a complete schema of your json everywhere your json gets parsed negates the advantages of json.

true_religion 2 hours ago | parent | next [-]

The main advantage of json is that it’s human readable and writable. Beyond that, it has no notion of user created data types so anyone using it has to do custom unmarshalling to get a type apart from sting, number, dict and list.

hueho an hour ago | parent | prev [-]

Most JSON libraries in typed languages require this for data binding to complex types though.

cyral 4 hours ago | parent | prev [-]

I've been doing this for so long and never knew there was a reviver param, thanks - that is super useful.

rimunroe 4 hours ago | parent | prev | next [-]

> For example `JSON.parse(JSON.stringify(Temporal.PlainYearMonth.from({year:2026,month:1}))).subtract({ years: 1})` won't work, because it misses the prototype and is no longer an instance of Temporal.PlainYearMonth.

I don't know if I'm missing something, but that's exactly how I'd expect it to compose. Does the following do what you wanted your snippet to do?

  Temporal.PlainYearMonth.from(JSON.parse(JSON.stringify(Temporal.PlainYearMonth.from({year:2026,month:1}))))
JSON.stringify and JSON.parse should not be viewed as strict inverses of each other. `JSON.parse(JSON.stringify(x)) = x` is only true for a for a small category of values. That category is even smaller if parsing is happening in a different place than stringification because JSON doesn't specify runtime characteristics. This can lead to things like JSON parsing incorrect in JS because they're too large for JS to represent as a number.
gowld 4 hours ago | parent | prev [-]

Would a plain data object be an instance of PlainYearMonth?

If not, that regardless of being plain data or a serialized object with functions, you'd still need to convert it to the type you want.

Avamander 3 hours ago | parent | prev | next [-]

> Especially in JavaScript where I often share a lot of code between the client and the server and therefore also transfer data between them, I like to strictly separate data from logic

Which makes me wonder how it'll look like when interfacing with WASM. Better than Date?

chrisweekly 4 hours ago | parent | prev | next [-]

It should still be possible to continue using date-fns (or a similar lib) to suit your preference, right?

VanCoding 4 hours ago | parent [-]

yes, sure. probably there will even pop up a functional wrapper around the temporal API occasionally. But would've been nice if it was like this from the start.

5 hours ago | parent | prev [-]
[deleted]