Skip to content

Commit 30d7a94

Browse files
authored
Add typings for JSRecord and some unsafe extensions for JSObject (#487)
* Add typings for JSRecord and some unsafe extensions for JSObject This doesn't include JSObject extensions that require types that aren't defined yet, like JSSymbolicRecord. * Document js_linterop/lib/unsafe.dart * Fix WASM test * Reformat * Fix lints * Code review
1 parent 3bfae6c commit 30d7a94

7 files changed

Lines changed: 692 additions & 0 deletions

File tree

js_interop/lib/js_interop.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
export 'src/dart/date_time.dart';
6+
export 'src/dart/map.dart';
67
export 'src/date.dart';
8+
export 'src/record.dart';

js_interop/lib/src/dart/map.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.p
4+
5+
import 'dart:js_interop';
6+
7+
import '../record.dart';
8+
9+
/// Conversion from [Map] to [JSRecord].
10+
extension MapToJSRecord<V extends JSAny?> on Map<String, V> {
11+
/// Converts [this] to a [JSRecord] by cloning it.
12+
JSRecord<V> get toJSRecord => JSRecord.ofMap<V>(this);
13+
}

js_interop/lib/src/record.dart

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:js_interop';
6+
import 'dart:js_interop_unsafe';
7+
8+
import 'unsafe/object.dart';
9+
10+
/// A JavaScript "record type", or in other words an object that's used as a
11+
/// lightweight map.
12+
///
13+
/// This provides a map-like API and utilities for interacting with records, as
14+
/// well as a [toDart] method for converting it into a true map. It considers
15+
/// the object's keys to be its enumerable, own, string properties (following
16+
/// `Object.keys()`).
17+
///
18+
/// In most cases, JS records only accept string keys, and this type is
19+
/// optimized to make this case easy to work with by automatically wrapping and
20+
/// unwrapping [JSString]s. However, there are cases where [JSSymbol]s are used
21+
/// as keys, in which case [JSSymbolicRecord] may be used instead.
22+
///
23+
/// Because this is a JavaScript object it follows JavaScript ordering
24+
/// semantics. Specifically: all number-like keys come first in numeric order,
25+
/// then all string keys in insertion order.
26+
///
27+
/// **Note:** Like Dart collections, it's not guaranteed to be safe to modify
28+
/// this while iterating over it. Unlike Dart collections, it doesn't have any
29+
/// fail-safes to throw errors if this happens. So be extra careful!
30+
extension type JSRecord<V extends JSAny?>._(JSObject _) implements JSObject {
31+
/// Returns an iterable over tuples of the `key`/`value` pairs in this record.
32+
Iterable<(String, V)> get pairs =>
33+
JSObjectUnsafeExtension(this).entries.cast<(String, V)>();
34+
35+
/// See [Map.entries].
36+
Iterable<MapEntry<String, V>> get entries sync* {
37+
for (var (key, value) in pairs) {
38+
yield MapEntry(key, value);
39+
}
40+
}
41+
42+
/// See [Map.isEmpty].
43+
bool get isEmpty => length == 0;
44+
45+
/// See [Map.isNotEmpty].
46+
bool get isNotEmpty => length != 0;
47+
48+
/// See [Map.keys].
49+
Iterable<String> get keys sync* {
50+
for (var key in JSObjectUnsafeExtension(this).keys) {
51+
yield key.toDart;
52+
}
53+
}
54+
55+
/// See [Map.length].
56+
int get length => JSObjectUnsafeExtension(this).keys.length;
57+
58+
/// See [Map.values].
59+
Iterable<V> get values => JSObjectUnsafeExtension(this).values.cast<V>();
60+
61+
/// Creates a new Dart map with the same contents as this record.
62+
Map<String, V> get toDart => {for (var (key, value) in pairs) key: value};
63+
64+
/// Creates a new, empty record.
65+
factory JSRecord() => JSRecord._(JSObject());
66+
67+
/// Creates a [JSRecord] with the same keys and values as [other].
68+
static JSRecord<V> ofRecord<V extends JSAny?>(JSRecord<V> other) =>
69+
JSRecord<V>()..addAllFromRecord(other);
70+
71+
/// Like [Map.of], but creates a record.
72+
static JSRecord<V> ofMap<V extends JSAny?>(Map<String, V> other) =>
73+
JSRecord.fromEntries<V>(other.entries);
74+
75+
/// Like [Map.fromEntries], but creates a record.
76+
static JSRecord<V> fromEntries<V extends JSAny?>(
77+
Iterable<MapEntry<String, V>> entries,
78+
) => JSRecord<V>()..addEntries(entries);
79+
80+
/// Creates a new record and adds all the [pairs].
81+
///
82+
/// If multiple pairs have the same key, later occurrences overwrite the value
83+
/// of the earlier ones.
84+
static JSRecord<V> fromPairs<V extends JSAny?>(Iterable<(String, V)> pairs) =>
85+
JSRecord<V>()..addPairs(pairs);
86+
87+
/// See [Map.addAll].
88+
void addAll(Map<String, V> other) => addEntries(other.entries);
89+
90+
/// Adds all enumerable, own, string key/value pairs of [other] to this
91+
/// record.
92+
///
93+
/// If a key of [other] is already in this record, its value is overwritten.
94+
///
95+
/// The operation is equivalent to doing `this[key] = value` for each key and
96+
/// associated value in [other]. It iterates over [other], which must therefore
97+
/// not change during the iteration.
98+
void addAllFromRecord(JSRecord<V> other) => addPairs(other.pairs);
99+
100+
/// See [Map.addEntries].
101+
void addEntries(Iterable<MapEntry<String, V>> entries) {
102+
for (var MapEntry(key: key, value: value) in entries) {
103+
this[key] = value;
104+
}
105+
}
106+
107+
/// Adds all key/value pairs of [newPairs] to this record.
108+
///
109+
/// If a key of [newPairs] is already in this record, the corresponding value
110+
/// is overwritten.
111+
///
112+
/// The operation is equivalent to doing `this[entry.key] = entry.value` for
113+
/// each pair of the iterable.
114+
void addPairs(Iterable<(String, V)> newPairs) {
115+
for (var (key, value) in newPairs) {
116+
this[key] = value;
117+
}
118+
}
119+
120+
/// See [Map.clear].
121+
void clear() {
122+
for (var key in JSObjectUnsafeExtension(this).keys) {
123+
delete(key);
124+
}
125+
}
126+
127+
/// See [Map.containsKey].
128+
bool containsKey(Object? key) =>
129+
key is String && propertyIsEnumerable(key.toJS);
130+
131+
/// See [Map.containsValue].
132+
bool containsValue(Object? value) => values.any((actual) => actual == value);
133+
134+
/// See [Map.forEach].
135+
void forEach(void action(String key, V value)) {
136+
for (var (key, value) in pairs) {
137+
action(key, value);
138+
}
139+
}
140+
141+
/// See [Map.map].
142+
Map<K2, V2> map<K2, V2>(MapEntry<K2, V2> convert(String key, V value)) =>
143+
Map.fromEntries(pairs.map((pair) => convert(pair.$1, pair.$2)));
144+
145+
/// See [Map.putIfAbsent].
146+
V putIfAbsent(String key, V ifAbsent()) {
147+
if (containsKey(key)) return this[key]!;
148+
var result = ifAbsent();
149+
this[key] = result;
150+
return result;
151+
}
152+
153+
/// See [Map.remove].
154+
V? remove(Object? key) {
155+
if (!containsKey(key)) return null;
156+
var value = this[key];
157+
delete((key as String).toJS);
158+
return value;
159+
}
160+
161+
/// See [Map.removeWhere].
162+
void removeWhere(bool test(String key, V value)) {
163+
for (var (key, value) in pairs) {
164+
if (test(key, value)) delete(key.toJS);
165+
}
166+
}
167+
168+
/// See [Map.update].
169+
V update(String key, V update(V value), {V ifAbsent()?}) {
170+
if (containsKey(key)) {
171+
return this[key] = update(this[key]!);
172+
} else if (ifAbsent == null) {
173+
throw new ArgumentError("ifAbsent must be passed if the key is absent.");
174+
} else {
175+
return this[key] = ifAbsent();
176+
}
177+
}
178+
179+
/// See [Map.updateAll].
180+
void updateAll(V update(String key, V value)) {
181+
for (var (key, value) in pairs) {
182+
this[key] = update(key, value);
183+
}
184+
}
185+
186+
/// See [Map.operator[]].
187+
V? operator [](Object? key) =>
188+
key is String ? getProperty(key.toJS) as V? : null;
189+
190+
/// See [Map.operator[]=].
191+
void operator []=(String key, V value) => setProperty(key.toJS, value);
192+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:js_interop';
6+
7+
@JS('Object.assign')
8+
external void _assign(
9+
JSObject target, [
10+
JSAny? source1,
11+
JSAny? source2,
12+
JSAny? source3,
13+
JSAny? source4,
14+
]);
15+
16+
@JS('Object.entries')
17+
external JSArray<JSArray<JSAny?>> _entries(JSObject object);
18+
19+
@JS('Object.freeze')
20+
external void _freeze(JSObject object);
21+
22+
@JS('Reflect.get')
23+
external JSAny? _get(JSObject object, JSAny name, JSAny? thisArg);
24+
25+
@JS('Object.getOwnPropertyNames')
26+
external JSArray<JSString> _getOwnPropertyNames(JSObject object);
27+
28+
@JS('Object.getOwnPropertySymbols')
29+
external JSArray<JSSymbol> _getOwnPropertySymbols(JSObject object);
30+
31+
@JS('Object.hasOwn')
32+
external bool _hasOwn(JSObject object, JSAny property);
33+
34+
@JS('Object.keys')
35+
external JSArray<JSString> _keys(JSObject object);
36+
37+
@JS('Reflect.ownKeys')
38+
external JSArray<JSAny> _ownKeys(JSObject object);
39+
40+
@JS('Reflect.set')
41+
external bool _set(JSObject object, JSAny name, JSAny? value, JSAny? thisArg);
42+
43+
@JS('Object.values')
44+
external JSArray<JSAny?> _values(JSObject object);
45+
46+
/// Additional instance methods for the `dart:js_interop` [JSObject] type meant
47+
/// to be used when the names of properties or methods are not known statically.
48+
extension JSObjectUnsafeExtension on JSObject {
49+
/// See [`Object.entries()`].
50+
///
51+
/// [`Object.entries()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
52+
List<(String, JSAny?)> get entries => [
53+
for (var entry in _entries(this).toDart)
54+
((entry[0] as JSString).toDart, entry[1]),
55+
];
56+
57+
/// See [`Reflect.ownKeys()`].
58+
///
59+
/// The return value contains only [JSString]s and [JSSymbol]s.
60+
///
61+
/// [`Reflect.ownKeys()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/ownKeys
62+
List<JSAny> get ownKeys => _ownKeys(this).toDart;
63+
64+
/// See [`Object.getOwnPropertyNames()`].
65+
///
66+
/// [`Object.getOwnPropertyNames()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames
67+
List<JSString> get ownPropertyNames => _getOwnPropertyNames(this).toDart;
68+
69+
/// See [`Object.getOwnPropertySymbols()`].
70+
///
71+
/// [`Object.getOwnPropertySymbols()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols
72+
List<JSSymbol> get ownPropertySymbols => _getOwnPropertySymbols(this).toDart;
73+
74+
/// See [`Object.keys()`].
75+
///
76+
/// [`Object.keys()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
77+
List<JSString> get keys => _keys(this).toDart;
78+
79+
/// See [`Object.values()`].
80+
///
81+
/// [`Object.values()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
82+
List<JSAny?> get values => _values(this).toDart;
83+
84+
/// See [`Object.assign()`].
85+
///
86+
/// [`Object.assign()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
87+
void assign([
88+
JSObject? source1,
89+
JSObject? source2,
90+
JSObject? source3,
91+
JSObject? source4,
92+
]) => _assign(this, source1, source2, source3, source4);
93+
94+
/// See [`Object.freeze()`].
95+
///
96+
/// [`Object.freeze()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
97+
void freeze() => _freeze(this);
98+
99+
/// See [`Reflect.get()`].
100+
///
101+
/// The [name] must be a [JSString] or a [JSSymbol].
102+
///
103+
/// [`Reflect.get()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
104+
R getPropertyWithThis<R extends JSAny?>(JSAny name, JSAny? thisArg) =>
105+
_get(this, name, thisArg) as R;
106+
107+
/// See [`Object.hasOwn()`].
108+
///
109+
/// The [name] must be a [JSString] or a [JSSymbol].
110+
///
111+
/// [`Object.hasOwn()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn
112+
bool hasOwnProperty(JSAny name) => _hasOwn(this, name);
113+
114+
/// See [`Reflect.set()`].
115+
///
116+
/// The [name] must be a [JSString] or a [JSSymbol].
117+
///
118+
/// [`Reflect.set()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set
119+
bool setPropertyWithThis(JSAny name, JSAny? thisArg, JSAny? value) =>
120+
_set(this, name, value, thisArg);
121+
122+
/// See [`Object.isPrototypeOf()`].
123+
///
124+
/// [`Object.isPrototypeOf()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf
125+
external bool isPrototypeOf(JSObject other);
126+
127+
/// See [`Object.propertyIsEnumerable()`].
128+
///
129+
/// The [name] must be a [JSString] or a [JSSymbol].
130+
///
131+
/// [`Object.propertyIsEnumerable()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable
132+
external bool propertyIsEnumerable(JSAny name);
133+
}

js_interop/lib/unsafe.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// Like `dart:js_interop_unsafe`, this library contains utilities that treat JS
6+
/// objects as arbitrary sets of properties as well as those that expose JS's
7+
/// runtime reflection capabilities. It should be used with care as it can
8+
/// invalidate assumptions made by the statically type-annotated JS APIs used
9+
/// elsewhere.
10+
library;
11+
12+
export 'src/unsafe/object.dart';

0 commit comments

Comments
 (0)