Skip to content

Conversation

@nex3
Copy link
Member

@nex3 nex3 commented Dec 10, 2025

This doesn't include JSObject extensions that require types that
aren't defined yet, like JSSymbolicRecord.

This doesn't include JSObject extensions that require types that
aren't defined yet, like JSSymbolicRecord.
@kevmoo
Copy link
Member

kevmoo commented Dec 10, 2025

We should do better on those JS failures in Wasm: dart-lang/sdk#62218

/// The operation is equivalent to doing `this[key] = value` for each key and
/// associated value in [other]. It iterates over [other], which must therefore
/// not change during the iteration.
void addAllRecord(JSRecord<V> other) => addPairs(other.pairs);
Copy link
Contributor

Choose a reason for hiding this comment

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

tiny nit: maybe addAllFromRecord instead?

@JS('Object.values')
external JSArray<JSAny?> _values(JSObject object);

/// Additional instance methods for the `dart:js_interop` [interop.JSObject]
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove interop.

/// See [`Object.entries()`].
///
/// [`Object.entries()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
List<(String, JSAny?)> get entries => [
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts on making this a generic method instead so that the second tuple member can be a generic (and therefore avoid the need for a cast list)?

/// The [name] must be a [JSString] or a [JSSymbol].
///
/// [`Reflect.get()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
R getPropertyWithThis<R extends JSAny?>(JSAny name, JSAny? thisArg) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this and setPropertyWithThis are sufficiently different in name that we may want a Reflect type instead, especially if we're planning to add more Reflect members. Thoughts?

);
});

test("addPairs()", () {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: did you mean to name this as clear()?

expect(record.containsKey("foo"), isTrue);
expect(record.containsKey("baz"), isFalse);
});

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: test for containsValue

() =>
expect(object.getPropertyWithThis("foo".toJS, object), equals(1.toJS)),
);

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: test for getOwnProperty

@kevmoo
Copy link
Member

kevmoo commented Dec 11, 2025

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces JSRecord as a map-like wrapper for JS objects and adds several unsafe extensions for JSObject. The implementation is generally good, but I've found a few areas for improvement, mainly related to performance in JSRecord and some minor issues in documentation and tests. My review includes suggestions to optimize method implementations and fix typos.

@nikeokoronkwo
Copy link
Collaborator

Thank you for providing this! It's something I've really wanted officially in Dart JS Interop.

I wanted to ask why we only make use of JSRecord with string keys. According to the ES Spec, JS Records can have a string, number or symbol as a key, the same way a Dart Map can have any Dart type. Packages like zod, reduxjs and some others depend on this functionality, and it would be nice to ensure we do not prevent such packages from having undefined behavior (or just not work well at all) in Dart, or prevent users from being able to use such functionality (I currently use this functionality when working with OpenAPI docs for instance, and iterating through error code indices).

I'm mainly saying this because eventually, I'd want to replace the generated types we make using the web generator to these ones, so it'd be nice if these types could match rather than having to remake the same types.

@nex3
Copy link
Member Author

nex3 commented Dec 15, 2025

JS objects can't actually have numeric keys; numbers are coerced to strings. You can test this by running the following in the dev console:

const obj = {};
obj[1] = 1;
obj["1"]

Symbols are another matter. I plan to bring over another type, JSSymbolicRecord, which uses JSAny as its key type in place of String. I think it's valuable to have access to both, because there are many places where only strings are expected or allowed, and it's much simpler to work with these types when you don't have to manually wrap and unwrap JSString every time. Then we can use JSSymbolicRecord for places where the higher generality is allowed.

@nikeokoronkwo
Copy link
Collaborator

JS objects can't actually have numeric keys; numbers are coerced to strings.

Agreed, but numeric indexes could also still work, and people/js package authors still rely on this functionality anyways.

The main issue I'm having is that the packages I mentioned use custom types (usually unions, but there are instances of enums as well, which are usually numbers underlying) as indices, which do reduce to string eventually, but at compile time it isn't so clear. Especially with the way dart interop code is generated at the moment, such types, if unions, would show as JSAny, and would therefore not fit in such cases (even if we ignored the type, users may want to index with such values rather than having to coerce directly into a string/number).

Symbols are another matter. I plan to bring over another type, JSSymbolicRecord, which uses JSAny as its key type in place of String. I think it's valuable to have access to both, because there are many places where only strings are expected or allowed, and it's much simpler to work with these types when you don't have to manually wrap and unwrap JSString every time. Then we can use JSSymbolicRecord for places where the higher generality is allowed.

In that case, would it be fine if we have JSRecord as is (maybe with a warning or something else not to use this directly but to use the child types), and then have a JSStringRecord and JSSymbolicRecord inheriting from JSRecord, which the majority of users would use? That way, packages that don't use strings for indexing can still work, and the majority of people can still use.

@nex3
Copy link
Member Author

nex3 commented Dec 15, 2025

Agreed, but numeric indexes could also still work, and people/js package authors still rely on this functionality anyways.

In principle, you can pass any value to an object key and it'll work—but it'll also coerce the value to a string. This is the way of a loosely-typed dynamic language. But JS interop generally tries to frame the APIs as strictly as possible, and a JS object can't meaningfully hold any keys that aren't strings or symbols. Since other key types will be coerced anyway, it's easy enough to do that coercion eagerly and pass them to the string API.

It's also worth noting that the JSRecord API simply can't work for other types because they can't be symmetric on input and output. You may be able to store a number or other object in an object's key, but when you go to retrieve or list it you'll always get a string.

In that case, would it be fine if we have JSRecord as is (maybe with a warning or something else not to use this directly but to use the child types), and then have a JSStringRecord and JSSymbolicRecord inheriting from JSRecord, which the majority of users would use? That way, packages that don't use strings for indexing can still work, and the majority of people can still use.

The substantial majority of record-style object uses in JS are specifically string keys (I count about 75% Record<string, ...> versus any other Record<...> declaration in TypeScript, and much of the latter seems to be union types with string representations anyway), and as such it makes sense for that to be the default type named JSRecord than for the two types to have symmetrical names.

@srujzs
Copy link
Contributor

srujzs commented Dec 16, 2025

@nikeokoronkwo Could the number/enum use-case be better handled if we had extra members that can set/update/etc. numeric keys? Since the numeric keys are stringified, should APIs like entries still return a Iterable<MapEntry<String, V>>? I agree with Natalie that the symmetry is off, but if we think it may make the numeric case easier if we had some members that operated on inputting numeric keys, that's an easier task. And unlike symbols, numbers get stringified so it seems like a natural fit too.

Re: the polymorphic option, we could have a base class for JSSymbolicRecord that would handle the numeric case too, but I don't think we should have the string version inherit from it as we would be forced to use JSString or do the conversions, which is pretty unideal.

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.

4 participants