Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@nex3
Copy link
Member

@nex3 nex3 commented Nov 6, 2025

  • Thanks for your contribution! Please replace this text with a description of what this PR is changing or adding and why, list any relevant issues, and review the contribution guidelines below.

  • I’ve reviewed the contributor guide and applied the relevant portions to this PR.
Contribution guidelines:

Many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.

Note: The Dart team is trialing Gemini Code Assist. Don't take its comments as final Dart team feedback. Use the suggestions if they're helpful; otherwise, wait for a human reviewer.

@kevmoo
Copy link
Member

kevmoo commented Nov 6, 2025

No tests? 😁

Copy link
Contributor

@srujzs srujzs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for adding this Natalie!

+1 to a quick test just to make sure we have the right bindings.

/// The [Date constructor] that returns the current date and time.
///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
external JSDate.now();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this might be a little confusing as intuitively, one would expect the static method Date.now to be called and not a constructor. What about JSDate.nowAsDate so that we don't need to rename now below?

external JSDate(int year, int month,
[int? day, int? hours, int? minutes, int? seconds, int? milliseconds]);

/// Dee [`Date.now()`].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: See

///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
/// [from a string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#date_string
external JSDate.parse(String dateString);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to now, what about parseAsDate instead so we can keep parse below?

///
/// [`Date.utc()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC
@JS('UTC')
external static int utc(int year,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We avoid renaming to lowercase with package:web e.g. URL, maybe worth doing the same?

Same comment on toIsoString, toUtcString, and toDartUtc.

/// [`Date.getDate()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDate
/// [`Date.setDate()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate
int get date => _getDate();
set date(int value) => _setDate(value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should keep the syntax the same as JS here by just exposing the methods instead of trying to be more Dart-y like we do in package:web.

Same comment everywhere else.

@nex3
Copy link
Member Author

nex3 commented Nov 7, 2025

Do you have an example of tests for thin wrapper declarations like this?


Since we're still defining the exact shape and conventions of this package, I'd like to make a bit of an impassioned plea that this not match web in hewing closely to the JS names and API shapes. For web itself, that design philosophy makes a lot of sense: the APIs are almost entirely autogenerated from interface definitions, so it would be onerous to manually make them match Dart conventions and have to keep those changes up to date. In addition, WebIDL already has a strong precedent of being implemented with few variations across multiple languages.

But js_interop has a different context which I think motivates a different design. Everything we expose here will be human-authored anyway, so the cost of a human hand making the design feel Dart-native is lower (and in fact I've already done most of this design work in my js_core repo). In fact, some of the APIs we intend to expose here (like JSRrecord) will necessarily be Dart-style, so there's a consistency case to be made for matching that across the package.

More importantly, though, these APIs are going to be foundational to the programming environment and commonly-used by authors doing any more than cursory Dart/JS interaction. Making them ergonomic to Dart authors and Dart conventions has an outsized benefit relative to the web APIs l, most of which will each individually be used less. That's particularly true for the APIs I'm planning to add to dart:js_interop, which this package should certainly match in style.

Along that line, I'd also make the case that dart:js_interop already leans towards using Dart conventions rather than JS. JSFunction has methods named callAsMethod and callAsConstructor when the JS equivalents are named apply() and construct(). While JSAny? does have an all-lowercase instanceof method, that naming is already inconsistent: it also defines instanceOfEquals.

The final case I'll make for using a more Dart-native style is for the ecosystem as a whole. Since their inception with collection, core-extension packages in the Dart ecosystem have served as the blueprints for what well-designed, idiomatic Dart APIs look like. We're in the very early days of the new (and likely final) shape of JS interop in Dart, and the APIs we release here and in my upcoming additions to the SDK will go a long way towards defining what a good JS API wrapper should look like everywhere. One of the things that makes the new JS interop system so exciting is that it makes it possible to use JS in a Dart-y way with near zero overhead. For a simple data type like Date this just looks like a few functions with different capitalization, but once we get into more complex and interesting APIs the usability difference will become profound. Let's not let the potential of this new system go to waste.

@srujzs
Copy link
Contributor

srujzs commented Nov 8, 2025

re: tests, we do this for some of the dart:js_interop APIs by just testing that a method gives the expected value with the expected type e.g. https://github.com/dart-lang/sdk/blob/59905c43f1a0394394ad5545ee439bcba63dea55/tests/lib/js/static_interop_test/js_types_test.dart#L237. Nothing fancy, just making sure there are no spelling or type issues. Ideally, we'd have these for all of the members in package:web, but that's impractical and because it's generated, it's less of an issue.


I think there's a valid argument to say this package is a better candidate to be Dart-y than package:web. The latter is generated and making that Dart-y would revive what made dart:html hard to maintain. Unlike package:web too, I expect the spec for the types in this package to be much more stable.

I'd still argue there are some benefits to having a model that translates directly to JS. I want and expect users to look for JS documentation when dealing with interop types, and I think it's easier for users to directly write those method/member names and have them exist/autocomplete properly. It's a (small) tax to have to look at the API and figure out how to make the equivalent JS call. I bet this tax goes up as the APIs get more complex and we get more opinionated.

Along that line, I'd also make the case that dart:js_interop already leans towards using Dart conventions rather than JS. JSFunction has methods named callAsMethod and callAsConstructor when the JS equivalents are named apply() and construct(). While JSAny? does have an all-lowercase instanceof method, that naming is already inconsistent: it also defines instanceOfEquals.

The instances where we do this are usually out of necessity. callAsFunction is actually call but we didn't want to expose that as call because then you can call it with () syntax, which sounds great, until you realize call takes in a this parameter as the first parameter. I suppose we could have a patched invoke method that actually invokes the function instead of .call, and I believe there's a request somewhere for that. callAsConstructor also doesn't use Reflect - it's a patched member that invokes the function with new. instanceOfString is more of a helper and we provide instanceof still (typeofEquals should really have an equivalent typeof member users can call). There are a few other odd cases in dart:js_interop like JSArray.add (instead of push) that have some specific context behind them.

All of this being said, helpers are great. That's partially why we have some of the "helpers" in package:web as well as conversion methods. But if we're only going to provide the "helper" version, it does mean we're going to need to be opinionated on how to expose JS interop and stay consistent with that opinion. More specifically to this PR, a third option is providing both e.g. the interop method that the more Dart-y APIs call and the Dart-y APIs themselves. That may be overkill though.

The final case I'll make for using a more Dart-native style is for the ecosystem as a whole. Since their inception with collection, core-extension packages in the Dart ecosystem have served as the blueprints for what well-designed, idiomatic Dart APIs look like. We're in the very early days of the new (and likely final) shape of JS interop in Dart, and the APIs we release here and in my upcoming additions to the SDK will go a long way towards defining what a good JS API wrapper should look like everywhere.

I think I feel slightly different about interop-related packages because they're inherently dealing with another language. This argument makes sense for any helpers we add, but they shouldn't be complete replacements.

For a simple data type like Date this just looks like a few functions with different capitalization, but once we get into more complex and interesting APIs the usability difference will become profound.

My above comment is relevant here: I expect the tax of trying to match the JS call to the Dart API we expose will go up as we look at the more complex cases but we can talk about those when we get there.

@nex3
Copy link
Member Author

nex3 commented Nov 10, 2025

I think the tax of having to remember not to use Dart naming conventions when writing Dart code is likely to be higher than the tax of not exactly matching the JS API names, especially since no matter what we can't match JS names exactly—as you point out, there will always be cases like call that can't be written in Dart for technical reasons. Authors will never be able to rely on names being JS-style without caveats, but they would be able to rely on them being Dart-style without caveats.

I feel more strongly about this for places where the JS names outright violate Dart naming conventions (like Date.utc) than I do for cases where I'm reusing a JS name to do something slightly different (like Date.now). Maybe we can compromise on only doing the former and renaming the latter to Date.nowAsDate and so on?

@srujzs
Copy link
Contributor

srujzs commented Nov 11, 2025

Authors will never be able to rely on names being JS-style without caveats, but they would be able to rely on them being Dart-style without caveats.

I agree that there will always be exceptions due to the nature of keywords/Dart semantics, and I can see the argument that e.g. reaching for the all-uppercase version is less intuitive than the camel-cased version, but I think a strict adherence to Dart style for interop, e.g. getHours and setHours being moved to a getter-setter pair, can be less intuitive and requires reading the API documentation. Maybe my mental model is wrong, but I assume interop users start from wanting to call a JS member/functionality and work their way backwards.

I feel more strongly about this for places where the JS names outright violate Dart naming conventions (like Date.utc) than I do for cases where I'm reusing a JS name to do something slightly different (like Date.now). Maybe we can compromise on only doing the former and renaming the latter to Date.nowAsDate and so on?

Sounds good, I don't have a strong opinion on capitalization. utc should be barely less findable than UTC.

@nex3 nex3 requested a review from srujzs November 11, 2025 20:31
@nex3 nex3 force-pushed the date branch 2 times, most recently from f779434 to ef7e339 Compare November 11, 2025 20:36
int? seconds,
int? milliseconds,
]) {
var ms = switch ((day, hours, minutes, seconds, milliseconds)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love me some switch!


import 'package:js_interop/js_interop.dart';

final Matcher _isDate = predicate(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the isA matcher w/ having will give nicer errors when this fails.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think isA works with JS interop types, unfortunately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gah! Right. Ugh.

@nex3
Copy link
Member Author

nex3 commented Nov 11, 2025

I've added tests, updated member names, and added a new Date.utcDate constructor (and renamed Date() to Date.localDate() to make the distinction explicit). PTAL.

Maybe my mental model is wrong, but I assume interop users start from wanting to call a JS member/functionality and work their way backwards.

I wonder if this is the core disconnect here. When I'm writing interop code, the biggest point of friction is usually context switching. This is especially true when I'm dealing with APIs that are weird shapes for historical reasons—remembering that DateTime.now() gets me the current date in Dart but Date.now() gets an int in JS is a speedbump, remembering that some JS methods inherited Java's get... and set... before the language added real properties is a speedbump, and so on. It's even worse if I'm writing one language and dealing with quirks from a different one.

The thing that eases this pain is API consistency, which is why I'm advocating for it so fervently. If I can know that when I'm writing Dart code, APIs will be Dart-shaped, historical artifacts will be smoothed out, and names will follow Dart conventions, my life becomes much easier.

By contrast, having the APIs match precisely across languages is relatively low value to me (as long as the correspondence is documented). I rarely access APIs entirely by memory, even when I'm working in a single language that I'm familiar with (even when I'm using APIs I personally created!). I rely on documentation, either through the web or through my editor, to remind me the precise names or arguments of whatever I'm interacting with. If I'm writing JS interop code in Dart, I'd be looking those up in the Dart documentation anyway, so having them be a little more different from the JS in exchange for them being a little more ergonomic at every use-site is a tradeoff that's well worth its cost to me.

@srujzs
Copy link
Contributor

srujzs commented Nov 12, 2025

I wonder if this is the core disconnect here.

Probably. :D I also think this will differ between folks who will use these types regularly versus folks who use these once.

If I'm writing JS interop code in Dart, I'd be looking those up in the Dart documentation anyway, so having them be a little more different from the JS in exchange for them being a little more ergonomic at every use-site is a tradeoff that's well worth its cost to me.

I suppose one benefit types in this package will have over package:web is they'll be more intuitive and require less reason to go look for documentation if the name makes it fairly obvious.


But okay, so that we make progress, let's go with the strategy that you propose and make things more Dart-y and ergonomic where it makes sense. In this PR that means capitalization modifications and moving obvious methods to getters/setters.

I expect us to do a pre-release, and we can get more input (if any) from other users on the conventions then before releasing a 1.0.0.

///
/// [Date constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
/// [individual component integers]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#individual_date_and_time_component_values
factory JSDate.utcDate(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we think JSDate.fromMillisecondsSinceEpoch(JSDate.utcAsMillisecondsSinceEpoch(...)) is bad enough that we should include this? I mostly ask because our treatment of nullability makes this factory a bit more complex. If we do want this we should document that treatment (e.g. "if day is null, we only pass year and month. if not and if hours is nullable,...").

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think given that there's already a local-time constructor, having a symmetrical one for UTC makes sense (especially because it's often going to be the safer option).

If we do want this we should document that treatment (e.g. "if day is null, we only pass year and month. if not and if hours is nullable,...").

It actually works in the opposite way: it passes all arguments through the last non-null. This makes the observable behavior identical to localDate: if you pass any intermediate nulls, you get an "Invalid Date". Since the behaviors match (and match the behavior of calling JSDate.fromMillisecondsSinceEpoch(JSDate.utcAsMillisecondsSinceEpoch(...)) I don't think it needs special documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, I thought you were trying to avoid passing null regardless of whether the user passed it.

My follow-up question would've been "why not implement the body as JSDate.fromMillisecondsSinceEpoch(JSDate.utcAsMillisecondsSinceEpoch(...))", but it looks like someone decided to make the default for days 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#parameters, so there is an observable difference between passing null and not passing anything. This LGTM then.

if you pass any intermediate nulls, you get an "Invalid Date"

Is this true? I'm constructing e.g. Date(2025, 11, null, 1) and that results in a valid date.

@nex3
Copy link
Member Author

nex3 commented Nov 12, 2025

I expect us to do a pre-release, and we can get more input (if any) from other users on the conventions then before releasing a 1.0.0.

Sounds good, collecting feedback from users at large is definitely the most robust way to answer this question 🙂.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants