-
Notifications
You must be signed in to change notification settings - Fork 45
Add typings for JSRecord and some unsafe extensions for JSObject #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nex3
wants to merge
6
commits into
dart-lang:main
Choose a base branch
from
nex3:jsrecord
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
7efd84d
Add typings for JSRecord and some unsafe extensions for JSObject
nex3 92338e7
Document js_linterop/lib/unsafe.dart
nex3 f8e3e72
Fix WASM test
nex3 eec8dc5
Reformat
nex3 6a29a0a
Fix lints
nex3 0c7cec2
Code review
nex3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file.p | ||
|
|
||
| import 'dart:js_interop'; | ||
|
|
||
| import '../record.dart'; | ||
|
|
||
| /// Conversion from [Map] to [JSRecord]. | ||
| extension MapToJSRecord<V extends JSAny?> on Map<String, V> { | ||
| /// Converts [this] to a [JSRecord] by cloning it. | ||
| JSRecord<V> get toJSRecord => JSRecord.ofMap<V>(this); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
||
| import 'dart:js_interop'; | ||
| import 'dart:js_interop_unsafe'; | ||
|
|
||
| import 'unsafe/object.dart'; | ||
|
|
||
| /// A JavaScript "record type", or in other words an object that's used as a | ||
| /// lightweight map. | ||
| /// | ||
| /// This provides a map-like API and utilities for interacting with records, as | ||
| /// well as a [toDart] method for converting it into a true map. It considers | ||
| /// the object's keys to be its enumerable, own, string properties (following | ||
| /// `Object.keys()`). | ||
| /// | ||
| /// In most cases, JS records only accept string keys, and this type is | ||
| /// optimized to make this case easy to work with by automatically wrapping and | ||
| /// unwrapping [JSString]s. However, there are cases where [JSSymbol]s are used | ||
| /// as keys, in which case [JSSymbolicRecord] may be used instead. | ||
| /// | ||
| /// Because this is a JavaScript object it follows JavaScript ordering | ||
| /// semantics. Specifically: all number-like keys come first in numeric order, | ||
| /// then all string keys in insertion order. | ||
| /// | ||
| /// **Note:** Like Dart collections, it's not guaranteed to be safe to modify | ||
| /// this while iterating over it. Unlike Dart collections, it doesn't have any | ||
| /// fail-safes to throw errors if this happens. So be extra careful! | ||
| extension type JSRecord<V extends JSAny?>._(JSObject _) implements JSObject { | ||
| /// Returns an iterable over tuples of the `key`/`value` pairs in this record. | ||
| Iterable<(String, V)> get pairs => | ||
| JSObjectUnsafeExtension(this).entries.cast<(String, V)>(); | ||
|
|
||
| /// See [Map.entries]. | ||
| Iterable<MapEntry<String, V>> get entries sync* { | ||
| for (var (key, value) in pairs) { | ||
| yield MapEntry(key, value); | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.isEmpty]. | ||
| bool get isEmpty => length == 0; | ||
|
|
||
| /// See [Map.isNotEmpty]. | ||
| bool get isNotEmpty => length != 0; | ||
|
|
||
| /// See [Map.keys]. | ||
| Iterable<String> get keys sync* { | ||
| for (var key in JSObjectUnsafeExtension(this).keys) { | ||
| yield key.toDart; | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.length]. | ||
| int get length => JSObjectUnsafeExtension(this).keys.length; | ||
|
|
||
| /// See [Map.values]. | ||
| Iterable<V> get values => JSObjectUnsafeExtension(this).values.cast<V>(); | ||
|
|
||
| /// Creates a new Dart map with the same contents as this record. | ||
| Map<String, V> get toDart => {for (var (key, value) in pairs) key: value}; | ||
|
|
||
| /// Creates a new, empty record. | ||
| factory JSRecord() => JSRecord._(JSObject()); | ||
|
|
||
| /// Creates a [JSRecord] with the same keys and values as [other]. | ||
| static JSRecord<V> ofRecord<V extends JSAny?>(JSRecord<V> other) => | ||
| JSRecord<V>()..addAllFromRecord(other); | ||
|
|
||
| /// Like [Map.of], but creates a record. | ||
| static JSRecord<V> ofMap<V extends JSAny?>(Map<String, V> other) => | ||
| JSRecord.fromEntries<V>(other.entries); | ||
|
|
||
| /// Like [Map.fromEntries], but creates a record. | ||
| static JSRecord<V> fromEntries<V extends JSAny?>( | ||
| Iterable<MapEntry<String, V>> entries, | ||
| ) => JSRecord<V>()..addEntries(entries); | ||
|
|
||
| /// Creates a new record and adds all the [pairs]. | ||
| /// | ||
| /// If multiple pairs have the same key, later occurrences overwrite the value | ||
| /// of the earlier ones. | ||
| static JSRecord<V> fromPairs<V extends JSAny?>(Iterable<(String, V)> pairs) => | ||
| JSRecord<V>()..addPairs(pairs); | ||
|
|
||
| /// See [Map.addAll]. | ||
| void addAll(Map<String, V> other) => addEntries(other.entries); | ||
|
|
||
| /// Adds all enumerable, own, string key/value pairs of [other] to this | ||
| /// record. | ||
| /// | ||
| /// If a key of [other] is already in this record, its value is overwritten. | ||
| /// | ||
| /// 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 addAllFromRecord(JSRecord<V> other) => addPairs(other.pairs); | ||
|
|
||
| /// See [Map.addEntries]. | ||
| void addEntries(Iterable<MapEntry<String, V>> entries) { | ||
| for (var MapEntry(key: key, value: value) in entries) { | ||
| this[key] = value; | ||
| } | ||
| } | ||
|
|
||
| /// Adds all key/value pairs of [newPairs] to this record. | ||
| /// | ||
| /// If a key of [newPairs] is already in this record, the corresponding value | ||
| /// is overwritten. | ||
| /// | ||
| /// The operation is equivalent to doing `this[entry.key] = entry.value` for | ||
| /// each pair of the iterable. | ||
| void addPairs(Iterable<(String, V)> newPairs) { | ||
| for (var (key, value) in newPairs) { | ||
| this[key] = value; | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.clear]. | ||
| void clear() { | ||
| for (var key in JSObjectUnsafeExtension(this).keys) { | ||
| delete(key); | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.containsKey]. | ||
| bool containsKey(Object? key) => | ||
| key is String && propertyIsEnumerable(key.toJS); | ||
|
|
||
| /// See [Map.containsValue]. | ||
| bool containsValue(Object? value) => values.any((actual) => actual == value); | ||
|
|
||
| /// See [Map.forEach]. | ||
| void forEach(void action(String key, V value)) { | ||
| for (var (key, value) in pairs) { | ||
| action(key, value); | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.map]. | ||
| Map<K2, V2> map<K2, V2>(MapEntry<K2, V2> convert(String key, V value)) => | ||
| Map.fromEntries(pairs.map((pair) => convert(pair.$1, pair.$2))); | ||
|
|
||
| /// See [Map.putIfAbsent]. | ||
| V putIfAbsent(String key, V ifAbsent()) { | ||
| if (containsKey(key)) return this[key]!; | ||
| var result = ifAbsent(); | ||
| this[key] = result; | ||
| return result; | ||
| } | ||
|
|
||
| /// See [Map.remove]. | ||
| V? remove(Object? key) { | ||
| if (!containsKey(key)) return null; | ||
| var value = this[key]; | ||
| delete((key as String).toJS); | ||
| return value; | ||
| } | ||
|
|
||
| /// See [Map.removeWhere]. | ||
| void removeWhere(bool test(String key, V value)) { | ||
| for (var (key, value) in pairs) { | ||
| if (test(key, value)) delete(key.toJS); | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.update]. | ||
| V update(String key, V update(V value), {V ifAbsent()?}) { | ||
| if (containsKey(key)) { | ||
| return this[key] = update(this[key]!); | ||
| } else if (ifAbsent == null) { | ||
| throw new ArgumentError("ifAbsent must be passed if the key is absent."); | ||
| } else { | ||
| return this[key] = ifAbsent(); | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.updateAll]. | ||
| void updateAll(V update(String key, V value)) { | ||
| for (var (key, value) in pairs) { | ||
| this[key] = update(key, value); | ||
| } | ||
| } | ||
|
|
||
| /// See [Map.operator[]]. | ||
| V? operator [](Object? key) => | ||
| key is String ? getProperty(key.toJS) as V? : null; | ||
|
|
||
| /// See [Map.operator[]=]. | ||
| void operator []=(String key, V value) => setProperty(key.toJS, value); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
||
| import 'dart:js_interop'; | ||
|
|
||
| @JS('Object.assign') | ||
| external void _assign( | ||
| JSObject target, [ | ||
| JSAny? source1, | ||
| JSAny? source2, | ||
| JSAny? source3, | ||
| JSAny? source4, | ||
| ]); | ||
|
|
||
| @JS('Object.entries') | ||
| external JSArray<JSArray<JSAny?>> _entries(JSObject object); | ||
|
|
||
| @JS('Object.freeze') | ||
| external void _freeze(JSObject object); | ||
|
|
||
| @JS('Reflect.get') | ||
| external JSAny? _get(JSObject object, JSAny name, JSAny? thisArg); | ||
|
|
||
| @JS('Object.getOwnPropertyNames') | ||
| external JSArray<JSString> _getOwnPropertyNames(JSObject object); | ||
|
|
||
| @JS('Object.getOwnPropertySymbols') | ||
| external JSArray<JSSymbol> _getOwnPropertySymbols(JSObject object); | ||
|
|
||
| @JS('Object.hasOwn') | ||
| external bool _hasOwn(JSObject object, JSAny property); | ||
|
|
||
| @JS('Object.keys') | ||
| external JSArray<JSString> _keys(JSObject object); | ||
|
|
||
| @JS('Reflect.ownKeys') | ||
| external JSArray<JSAny> _ownKeys(JSObject object); | ||
|
|
||
| @JS('Reflect.set') | ||
| external bool _set(JSObject object, JSAny name, JSAny? value, JSAny? thisArg); | ||
|
|
||
| @JS('Object.values') | ||
| external JSArray<JSAny?> _values(JSObject object); | ||
|
|
||
| /// Additional instance methods for the `dart:js_interop` [JSObject] type meant | ||
| /// to be used when the names of properties or methods are not known statically. | ||
| extension JSObjectUnsafeExtension on JSObject { | ||
| /// See [`Object.entries()`]. | ||
| /// | ||
| /// [`Object.entries()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries | ||
| List<(String, JSAny?)> get entries => [ | ||
| for (var entry in _entries(this).toDart) | ||
| ((entry[0] as JSString).toDart, entry[1]), | ||
| ]; | ||
|
|
||
| /// See [`Reflect.ownKeys()`]. | ||
| /// | ||
| /// The return value contains only [JSString]s and [JSSymbol]s. | ||
| /// | ||
| /// [`Reflect.ownKeys()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/ownKeys | ||
| List<JSAny> get ownKeys => _ownKeys(this).toDart; | ||
|
|
||
| /// See [`Object.getOwnPropertyNames()`]. | ||
| /// | ||
| /// [`Object.getOwnPropertyNames()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames | ||
| List<JSString> get ownPropertyNames => _getOwnPropertyNames(this).toDart; | ||
|
|
||
| /// See [`Object.getOwnPropertySymbols()`]. | ||
| /// | ||
| /// [`Object.getOwnPropertySymbols()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols | ||
| List<JSSymbol> get ownPropertySymbols => _getOwnPropertySymbols(this).toDart; | ||
|
|
||
| /// See [`Object.keys()`]. | ||
| /// | ||
| /// [`Object.keys()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys | ||
| List<JSString> get keys => _keys(this).toDart; | ||
|
|
||
| /// See [`Object.values()`]. | ||
| /// | ||
| /// [`Object.values()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values | ||
| List<JSAny?> get values => _values(this).toDart; | ||
|
|
||
| /// See [`Object.assign()`]. | ||
| /// | ||
| /// [`Object.assign()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign | ||
| void assign([ | ||
| JSObject? source1, | ||
| JSObject? source2, | ||
| JSObject? source3, | ||
| JSObject? source4, | ||
| ]) => _assign(this, source1, source2, source3, source4); | ||
kevmoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// See [`Object.freeze()`]. | ||
| /// | ||
| /// [`Object.freeze()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze | ||
| void freeze() => _freeze(this); | ||
|
|
||
| /// See [`Reflect.get()`]. | ||
| /// | ||
| /// 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) => | ||
srujzs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _get(this, name, thisArg) as R; | ||
|
|
||
| /// See [`Object.hasOwn()`]. | ||
| /// | ||
| /// The [name] must be a [JSString] or a [JSSymbol]. | ||
| /// | ||
| /// [`Object.hasOwn()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn | ||
| bool hasOwnProperty(JSAny name) => _hasOwn(this, name); | ||
|
|
||
| /// See [`Reflect.set()`]. | ||
| /// | ||
| /// The [name] must be a [JSString] or a [JSSymbol]. | ||
| /// | ||
| /// [`Reflect.set()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set | ||
| bool setPropertyWithThis(JSAny name, JSAny? thisArg, JSAny? value) => | ||
| _set(this, name, value, thisArg); | ||
|
|
||
| /// See [`Object.isPrototypeOf()`]. | ||
| /// | ||
| /// [`Object.isPrototypeOf()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf | ||
| external bool isPrototypeOf(JSObject other); | ||
|
|
||
| /// See [`Object.propertyIsEnumerable()`]. | ||
| /// | ||
| /// The [name] must be a [JSString] or a [JSSymbol]. | ||
| /// | ||
| /// [`Object.propertyIsEnumerable()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable | ||
| external bool propertyIsEnumerable(JSAny name); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
||
nex3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// Like `dart:js_interop_unsafe`, this library contains utilities that treat JS | ||
| /// objects as arbitrary sets of properties as well as those that expose JS's | ||
| /// runtime reflection capabilities. It should be used with care as it can | ||
| /// invalidate assumptions made by the statically type-annotated JS APIs used | ||
| /// elsewhere. | ||
| library; | ||
|
|
||
| export 'src/unsafe/object.dart'; | ||
nex3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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)?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd worry that this would make it too easy to hide the implicit cast. As a user and an API designer, I'd rather have it be explicit when I'm moving from type-definition-guaranteed types to expression-level-asserted types.
If this is a performance concern for
JSRecord.pairs, I'd rather address that by making it directly invoke the JSObject.entries().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the cast would still be explicit within the method, but maybe I don't fully understand
Maybe you're saying you'd rather have the user downcast as needed? To be clear, I'm imagining something like:
If one of the tuple members wasn't a
T, we'd see a cast failure from the cast.We could also change the type of
_entriesand I believeentry[1]will be a cast failure at that point.