As noted by @ptomato in https://github.com/tc39/proposal-temporal/issues/240#issuecomment-614936470:
There's a lot of
Temporal.Something.from()involved, making for long lines. For example, to take a DateTime and get midnight on that day, it's eitherdateTime.getDate().withTime(Temporal.Time.from('00:00'))(or, even longer if you want to avoid the from(),dateTime.with({ hour: 0, minute: 0, second: 0, microsecond: 0, millisecond: 0, nanosecond: 0 })). It might be more readable to reverse our decision not to take strings in methods like withTime(), plus(), etc.
I agreed in a follow-up comment:
Reading all the
.from()code makes the API look very cumbersome to work with, which _may_ hinder adoption. Playing around with some use cases in the console, I found myself automatically writing things likedate.withTime('10:00')and getting annoyed that it doesn't work.
From what I can tell, the decision to not allow strings in date.withTime() happened in #237, but I'm still not sure on the rationale for doing so. A lot of the cookbook examples look more complicated than they should with the current spec. I know which one of these two options I'd prefer to read in a codebase:
const date = Temporal.now.date();
date.withTime(Temporal.Time.from('09:00'));
date.withTime('09:00');
The main reason I think it should be reconsidered is that Absolute.prototype.inTimeZone() accepts a TimeZone OR a string (likewise for DateTime). So if I can write
abs.inTimeZone('Australia/Lord_Howe') instead of abs.inTimeZone(Temporal.TimeZone.from('Australia/Lord_Howe')), why can't I write date.withTime('13:45') or time.withDate('2020-11-01')?
_Edit for clarity after comments:_ My proposal is to allow whatever is allowed in the _first_ argument of Date.from() or Time.from(), respectively. This would include the correct Temporal objects (as it is now), strings, and property bags. Extra option arguments (e.g. overflow) would not be allowed — anyone wanting to use them can use the current technique of constructing the object via .from() directly.
I think TimeZones are a bit special, in terms of being treated as strings (e.g., in Intl), as well as not having any numbers/user data in them in the default case.
I've previously argued that we should be restrictive in terms of where we include "coercion" logic. I think it can get confusing if we allow lots of coercion by default in these argument positions, and that it's better to be explicit. I'm wondering, do you want options bags to be accepted in these positions too, as we previously did, before restricting coercion?
I'm wondering, do you want options bags to be accepted in these positions too, as we previously did, before restricting coercion?
To be honest, I hadn't thought about the options bags, mainly because I wasn't using them in my (limited) experiments. I don't see a problem with accepting them, because then it becomes easier to explain as a concept — e.g. "the argument passed to Date.prototype.withTime() is first run through Time.from()". I'd expect the same semantics and validation to apply then (i.e. if Time.from() doesn't accept it, then neither does Date.prototype.withTime()).
Note: I'm saying this purely from the point of view of an API consumer, not a spec writer. It's entirely possible there's a problematic case I've missed.
Strings are problematic when it comes to calendar systems, since they are unlikely to carry calendar annotations (#293). It is better to explicitly transition from strings into Temporal objects, rather than doing so implicitly in methods like Temporal.Time.prototype.withDate().
I see that, but I don't necessarily agree that's a concern in the particular case of withTime and withDate. If you have date.withTime(string), then the resulting DateTime gets date's calendar. If you have time.withDate(string), then you can provide a calendar annotation in the string if we support that, and if we don't, then I wouldn't assume that an ISO string would give the resulting DateTime anything other than the ISO calendar.
Also, if a string is rejected, the user's just going to make an object from the string and pass it in anyways, which doesn't provide any additional information.
I support accepting strings here, both for ergonomics and for consistency with timezone or calendar parameters.
I don't think we should accept options bags in this position, because it makes for a confusing IDE autocomplete UX. Better to require undefined instead.
_Note for context: After this issue was created, the method names were changed from .withDate() and .withTime() to .toDateTime(), but the question still stands._
I don't think we should accept options bags in this position, because it makes for a confusing IDE autocomplete UX. Better to require
undefinedinstead.
@justingrant You've put forward the "IDE autocomplete experience" argument in a lot of cases around the API design. For the most part, I agree that we should make things easier for that scenario, and there are some nice discoverability tricks. (Your argument for .toXxx' in https://github.com/tc39/proposal-temporal/issues/747#issuecomment-689178182 converted me from my previous preference of date.withTime().)
However, in this case I disagree with the argument. IDE autocomplete experience is a good consideration, but should not be an overriding principle, especially when it decreases consistency in APIs.
Being consistent is more important than someone with an IDE getting slightly confused. It's much easier to explain and understand that date.toDateTime(_thing_) is effectively just syntactic sugar for date.toDateTime(Temporal.Time.from(_thing_)).
If a developer gets a slightly confusing result in autocomplete, they can check the documentation which explains the "syntactic sugar" concept. This won't be a new problem — I already have to check the docs for many TypeScript libraries with method overrides, because the multiple autocomplete results don't give the full story.
@gilmoreorless - I'm not sure I understand our disagreement here, could you explain? Are we actually agreeing?
My point was only about options bags as related to @littledan's comment :
I'm wondering, do you want options bags to be accepted in these positions too, as we previously did, before restricting coercion?
The specific case I think should be disallowed is where the same argument position can either be a Temporal-like object literal like {hour: 12} or an options bag like {disambiguation: 'constrain'}. I think this ambiguity is unnecessarily confusing. The IDE experience is a side effect of this confusing ambiguity, because seeing disambiguation or overflow along with year, month, and day in a single autocomplete list won't help the user figure out what kind of property bag they're supposed to put in that position.
2.
date.toDateTime(_thing_)is effectively just syntactic sugar fordate.toDateTime(Temporal.Time.from(_thing_)).
Agreed. We don't allow options bags in the first position of from, so IMHO we shouldn't allow them in withDate/withTime either.
I also agree that withDate/withTime should accept Date-like/Time-like property bags (not options bags!) in this position, because from does too.
IDE autocomplete experience is a good consideration, but should not be an overriding principle, especially when it decreases consistency in APIs.
I also agree here. IDE experience shouldn't be paramount-- it's just one consideration among many.
OK, where do we disagree? ;-)
OK, where do we disagree? ;-)
Errrgh, I think this mess has come about due to my misreading of one key word. When @littledan asked about _options_ bags, I was interpreting that as meaning _property_ bags. 🤦
So, to clarify:
.toDateTime() should still only take one parameter thing. The value of that is internally passed to Time.from() or Date.from() as needed..toDateTime() are things that are valid for the _first_ parameter of .from() of the respective class..from() manually.Sorry for all the confusion. 😄
@gilmoreorless No worries, after I posted my questions above it occurred to me that you might have misread that word. All's good. Do you want to revise your OP to include both strings and object property (not options!) bags in the proposal?
The general pattern here seems to be a good one if a conversion method requires additional data, then its argument should mirror from's first parameter: a string, a Temporal class instance, or a property bag object.
This is also relevant to the discussion we're having over in #747 about conversion methods overall.
Question for you: how do you think the proposed from-like pattern should be extended when there's more than one thing required? For example: Date.prototype.toLocalDateTime which requires both a required time zone and an optional time (defaults to start of day)?
_EDIT: added non-optional calendar per @sffc's latest proposal, and added Option 4 per @gibson042 suggestion in https://github.com/tc39/proposal-temporal/issues/592#issuecomment-689917856._
// Option 1: separate arguments
date.toLocalDateTime('America/Los_Angeles', '12:00');
date.toLocalDateTime('America/Los_Angeles'); // defaults to first moment of this day, usually but not always midnight
// Option 2: one argument: property bag with required `timeZone` property and optional `time` property
date.toLocalDateTime({timeZone: 'America/Los_Angeles', time: '12:00'});
date.toLocalDateTime({timeZone: 'America/Los_Angeles'}); // start of day, usually midnight
// Option 3: one bag with spread properties
date.toLocalDateTime({timeZone: 'America/Los_Angeles', ...Temporal.Time.from('12:00').getFields()});
date.toLocalDateTime({timeZone: 'America/Los_Angeles'}); // start of day, usually midnight
// Option 4: no conversion methods that always require multiple inputs
date.toDateTime('12:00').toLocalDateTime('America/Los_Angeles');
date.toDateTime().toLocalDateTime('America/Los_Angeles'); // start of day, usually midnight
Each of these options has pros and cons.
Option 1 is briefest and probably the most ergonomic, but it's not necessarily obvious from the API shape whether the timezone or the time should go first. This ambiguity might lead to occasional reversal bugs.
Option 2 has no ambiguity, but a time prop is most like what we decided NOT to do in #720. If we accepted that format in toDateTime/toLocalDateTime, then should it also be accepted in from? And if so, how would we deal with the possibility of conflicts which led to not adopting #720? EDIT 2020-09-11: #720 is now updated with changes that prevent conflicts.
Option 3 is, honestly, not a significant ergonomic benefit because you can't use a string for the time. It'd be easier for the caller to do this instead:
date.toDateTime('12:00').toLocalDateTime('America/Los_Angeles');
I'm not a fan of Option 4:
I'd lean towards Option 2 if we could resurrect #720 and resolve the problems with it. Otherwise Option 1.
_EDIT: one thing to keep in mind when choosing between the options above is how we'd handle MonthDay.prototype.toDate which requires a year but also might require an era or other calendar-specific properties when used with non-ISO calendars. If the pattern chosen above also works to make that case easier, that'd be ideal._
_EDIT: another thing to consider: Absolute.prototype.toDateTime and Absolute.prototype.toLocalDateTime already take two parameters: a time zone and a calendar. So Temporal's conversion pattern must already deal with multiple arguments. Ideally the pattern we choose for Date=>LocalDateTime would also work for Absolute=>LocalDateTime_
// Option 4: no conversion methods that always require multiple inputs
date.toDateTime('12:00').toLocalDateTime('America/Los_Angeles');
Yep agreed, that's another option to consider. I added it to https://github.com/tc39/proposal-temporal/issues/592#issuecomment-689885726 above to keep all the options in one place. I also added a variant showing how it looks when the time is set to the start-of-day default.
BTW, Absolute.prototype.toLocalDateTime and Absolute.prototype.toDateTime must accept two inputs: time zone and calendar. As must MonthDay->Date conversions (whatever we call the method) which sometimes requires era or other non-ISO calendar properties. So we can't actually limit all conversion methods to one input, although in Options 2 and Option 3 above, multiple logical inputs could be combined into a single property bag.
Do you want to revise your OP to include both strings and object property (not options!) bags in the proposal?
Done.
Question for you: how do you think the proposed
from-like pattern should be extended when there's more than one thing required? For example:Date.prototype.toLocalDateTimewhich requires both a required time zone and an optional time (defaults to start of day)?
My preference is Option 1 (followed by Option 2, but you're right that it causes other problems). I don't actually see the accidental reversal of arguments in Option 1 as being a big problem. Mainly because the types of arguments are different enough that reversing their order will cause parsing exceptions straight away. Though there is one case I can think of with ambiguity, when timeZone is just an offset:
date.toLocalDateTime('+10:00', '10:00') // Not identical arguments, but close enough to be confusing
My preference is Option 1 (followed by Option 2, but you're right that it causes other problems).
FYI I just updated #720 which may resolve the problems with Option 2.
Decision 2020-09-11:
relativeTo) then a string should also be accepted.from(someString) for the desired type.from.relativeTo is potentially problematic because it's the only place in Temporal where a string can result in different types. We'll follow up on this in #856. @gilmoreorless - Thanks for pushing on this idea. It's gonna happen! This small-seeming change may be the biggest ergonomic improvement we could have made in Temporal and it addresses the #1 feedback item: more brevity. Nice work!
Here is a list of APIs that would be affected, as far as I can tell:
According to the above description, the following APIs should not be affected, because a string can result in different types:
I would recommend not changing any of the Temporal.Calendar APIs, because they are meant to be called internally and only calendar implementers should care about them.
Speak up soon if you disagree with any of the above!
According to the above description, the following APIs should not be affected, because a string can result in different types:
- Temporal.{Date,DateTime,Duration,MonthDay,Time,YearMonth,ZonedDateTime}.prototype.with
I'm not sure I understand this limitation. Are you saying that the following would NOT be allowed, and if so then why?
plainDateTime.with('10:00');
plainDateTime.with('2020-01-01');
plainDateTime.with('2020-01');
Because of
relativeTois potentially problematic because it's the only place in Temporal where a string can result in different types.
with() has this problem as well. I didn't see anything in the notes about it.
Hmm, that's probably my fault for not circling back to edit this issue. My understanding from https://github.com/tc39/proposal-temporal/issues/720#issuecomment-695076744 was:
to address the underlying concern of brevity, we'll change with() methods to accept strings. We also noted a concern where passing a Temporal.Date object to with(), with a different calendar than
this, would "infect" thethisobject with its calendar, which we've come up with a solution for. I'll close this issue and open new ones for these items.
Is there a separate issue for "_we'll change with() methods to accept strings_" ?
My memory was that we agreed to interpret strings in with using a specific order, which I think was:
with methodNote that PlainDate is ahead of PlainDateTime because in the context of with we don't want a time to be introduced if the user didn't intentionally put the time in there. EDIT: actually, we'd need a little more logic, because PlainDate.from will accept a date/time string like 2020-01-01T12:30 without complaint. Regardless, in context of with we need to figure out a way for 2020-01-01 to be interpreted as a PlainDate, not a PlainDateTime.
Also note that this is different behavior than relativeTo where only ZonedDateTime and PlainDateTime are accepted.
OK, sounds good. I think I would like to open a separate issue for that, then. It's a much larger project than the rest of these changes.
Apparently I did already: #934