Proposal-temporal: For discussion: all-in-one additive conversion method

Created on 12 Sep 2020  路  6Comments  路  Source: tc39/proposal-temporal

JavaScript is weakly typed, somewhat unique among curlybrace languages. I wanted to suggest another option for additive conversion methods (#747, #889) that takes advantage of this aspect of JavaScript.

Proposal: Temporal.*.prototype.with() returns a dynamic type. The type is a function of the option bag arguments.

Examples:

// ISO MonthDay --> MonthDay
isoMonthDay.with({ day: 29 });

// ISO MonthDay --> Date
isoMonthDay.with({ year: 2020 });

// Japanese MonthDay --> Exception: not enough fields to construct a Date (missing era)
jpnMonthDay.with({ year: 2 });

// ISO MonthDay --> DateTime
isoMonthDay.with({ year: 2020, hour: 12 });

// ISO MonthDay --> DateTime
isoMonthDay.with({ year: 2020, hour: 12, minute: 0 });

// ISO MonthDay --> LocalDateTime
isoMonthDay.with({ year: 2020, hour: 12, minute: 0, timeZone: "America/Chicago" });

// ISO Time --> DateTime (assumes Time carries a calendar field)
isoTime.with({ year: 2020, month: 1, day: 1 });

// ISO Time --> Exception: not enough fields to construct a DateTime (missing month, day)
isoTime.with({ year: 2020 });

// ISO Time --> Exception: not enough fields to construct a LocalDateTime (missing year, month, day)
isoTime.with({ year: 2020, timeZone: "America/Chicago" });

// Absolute --> LocalDateTime
absolute.with({ timeZone: "America/Chicago", calendar: "iso8601" });

// Absolute --> Exception: not enough fields to construct a LocalDateTime (missing timeZone)
absolute.with({ calendar: "iso8601" });

// Absolute --> Exception: not enough fields to construct a LocalDateTime (missing calendar)
absolute.with({ timeZone: "America/Chicago" });

Algorithm to decide what type the with() function returns within the calendared types: Get a list of the calendar-sensitive fields required for each type. For example (fractional second units omitted for brevity):

  • ISO LocalDateTime: timeZone, year, month, day, hour, minute, second
  • ISO DateTime: year, month, day, hour, minute, second
  • ISO Date: year, month, day
  • ISO YearMonth: year, month
  • ISO MonthDay: month, day
  • ISO Time: hour, minute, second

When the with() function is called, take the current fields and union them with the fields in the argument. Pick the type from the list above with the most fields that are present. If there are multiple such types, pick the one with the fewest missing fields. For example, YearMonth with a day and hour will resolve to DateTime. Then, pass the options to that type's from function (via the calendar), and let that function decide whether or not to throw due to missing information.

For Absolute (the only non-calendared type): The two additive arguments are defined to be calendar and timeZone. No other types can be targeted except for LocalDateTime.


Yes, this solution looks a bit funny, and it doesn't play well with TypeScript. However, I do think it wins on brevity and clarity when reading code.

All 6 comments

I have expressed a desire for common naming _patterns_ between up-conversion/down-conversion/field-replacement methods, but this may be a bridge too far. For one thing, it risks making code too difficult to reason about (e.g., should date.with(fields).hour < 17 be expected to return a boolean or throw an exception?). For another, there's almost certain to be weirdness in the observability of operations, specifically regarding prototype chains and/or proxies (e.g., when does the check for a given field happen, and is it a direct Get or is it preceded by HasProperty, and would it stop checking early or always run through the complete list?).

I also have a question about why jpnMonthDay.with({ year: 2 }) should error for missing era鈥攚ouldn't that come from jpnMonthDay? I thought the primary purpose of with was to preserve all fields not overwritten by the argument.

I also have a question about why jpnMonthDay.with({ year: 2 }) should error for missing era鈥攚ouldn't that come from jpnMonthDay? I thought the primary purpose of with was to preserve all fields not overwritten by the argument.

jpnMonthDay is a Japanese month and day, but not year or era. It makes no sense to say "January 1, year 2"; you need to say "January 1, Reiwa 2".

I'm not a fan of this pattern because it seems actively unhelpful for developers who don't know Temporal well. Developers who don't know Temporal well have two questions about conversions:

  1. What types can I convert this to?
  2. What data do I need to add to obtain my desired type?

This pattern doesn't help developers answer either question. In particular it makes IDEs and TypeScript useless to help developers to answer those questions.

Also, in #574 a big reason that we originally switched to the toXxx pattern was because "what type do I want?" (aka (1) above) was the most important conversion question. By switching toXxx it made it easier to answer that question via IDEs, browser devtools, or simply scanning the table-of-contents in the docs. The proposed pattern doesn't do that.

Also, having the return type be dynamic in this way probably breaks TypeScript's ability to protect downstream code because TS won't know what the return type is. It may (not sure) be possible with some very tricky TS typing to encode your algorithm above into TS types, but I suspect there will be at least some cases where TS simply can't figure out what the return type is.

Finally, even without TS this pattern makes it really hard to reason about whether a particular method or property will be available on the return type or how that method will behave. This also makes code vulnerable to upstream changes. For example, if one more property is injected into the input, then the output type may change its behavior. That's not good.

I'm not a fan of this pattern because it seems actively unhelpful for developers who don't know Temporal well. Developers who don't know Temporal well have two questions about conversions:

  1. What types can I convert this to?
  2. What data do I need to add to obtain my desired type?

This pattern doesn't help developers answer either question. In particular it makes IDEs and TypeScript useless to help developers to answer those questions.

Also, in #574 a big reason that we originally switched to the toXxx pattern was because "what type do I want?" (aka (1) above) was the most important conversion question. By switching toXxx it made it easier to answer that question via IDEs, browser devtools, or simply scanning the table-of-contents in the docs. The proposed pattern doesn't do that.

That sounds like a good reason to dismiss this proposal.

A reasonable argument could be made that concerns over the dynamic typing are overblown. It's very much a classic argument over programming language theory. However, if we think the proposal isn't good for other reasons, like education/discoverability, then we can dismiss the proposal on those grounds.

Do we want to include this in the agenda for tomorrow? FWIW, I'm not a fan either, for the reasons Justin mentioned.

I'm satisfied with the reasons given in this thread. I will close. If anyone favors the all-in-one solution and wants to continue advocating for it, please re-open this issue.

Was this page helpful?
0 / 5 - 0 ratings