Skip to content

Commit ba4e49c

Browse files
committed
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.
1 parent 3d11c77 commit ba4e49c

File tree

7 files changed

+664
-0
lines changed

7 files changed

+664
-0
lines changed

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: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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 => JSObjectUnsafeExtension(this).entries.cast<(String, V)>();
33+
34+
/// See [Map.entries].
35+
Iterable<MapEntry<String, V>> get entries sync* {
36+
for (var (key, value) in pairs) {
37+
yield MapEntry(key, value);
38+
}
39+
}
40+
41+
/// See [Map.isEmpty].
42+
bool get isEmpty => length == 0;
43+
44+
/// See [Map.isNotEmpty].
45+
bool get isNotEmpty => length != 0;
46+
47+
/// See [Map.keys].
48+
Iterable<String> get keys sync* {
49+
for (var key in JSObjectUnsafeExtension(this).keys) {
50+
yield key.toDart;
51+
}
52+
}
53+
54+
/// See [Map.length].
55+
int get length => keys.length;
56+
57+
/// See [Map.values].
58+
Iterable<V> get values => JSObjectUnsafeExtension(this).values.cast<V>();
59+
60+
/// Creates a new Dart map with the same contents as this record.
61+
Map<String, V> get toDart => {for (var (key, value) in pairs) key: value};
62+
63+
/// Creates a new, empty record.
64+
factory JSRecord() => JSRecord._(JSObject());
65+
66+
/// Creates a [JSRecord] with the same keys and values as [other].
67+
static JSRecord<V> ofRecord<V extends JSAny?>(JSRecord<V> other) =>
68+
JSRecord<V>()..addAllRecord(other);
69+
70+
/// Like [Map.of], but creates a record.
71+
static JSRecord<V> ofMap<V extends JSAny?>(Map<String, V> other) =>
72+
JSRecord.fromEntries<V>(other.entries);
73+
74+
/// Like [Map.fromEntries], but creates a record.
75+
static JSRecord<V> fromEntries<V extends JSAny?>(
76+
Iterable<MapEntry<String, V>> entries,
77+
) => JSRecord<V>()..addEntries(entries);
78+
79+
/// Creates a new record and adds all the [pairs].
80+
///
81+
/// If multiple pairs have the same key, later occurrences overwrite the value
82+
/// of the earlier ones.
83+
static JSRecord<V> fromPairs<V extends JSAny?>(Iterable<(String, V)> pairs) =>
84+
JSRecord<V>()..addPairs(pairs);
85+
86+
/// See [Map.addAll].
87+
void addAll(Map<String, V> other) => addEntries(other.entries);
88+
89+
/// Adds all enumerable, own, string key/value pairs of [other] to this
90+
/// record.
91+
///
92+
/// If a key of [other] is already in this record, its value is overwritten.
93+
///
94+
/// The operation is equivalent to doing `this[key] = value` for each key and
95+
/// associated value in [other]. It iterates over [other], which must therefore
96+
/// not change during the iteration.
97+
void addAllRecord(JSRecord<V> other) => addPairs(other.pairs);
98+
99+
/// See [Map.addEntries].
100+
void addEntries(Iterable<MapEntry<String, V>> entries) {
101+
for (var MapEntry(key: key, value: value) in entries) {
102+
this[key] = value;
103+
}
104+
}
105+
106+
/// Adds all key/value pairs of [newPairs] to this record.
107+
///
108+
/// If a key of [newPairs] is already in this record, the corresponding value
109+
/// is overwritten.
110+
///
111+
/// The operation is equivalent to doing `this[entry.key] = entry.value` for
112+
/// each pair of the iterable.
113+
void addPairs(Iterable<(String, V)> newPairs) {
114+
for (var (key, value) in newPairs) {
115+
this[key] = value;
116+
}
117+
}
118+
119+
/// See [Map.clear].
120+
void clear() {
121+
for (var key in keys) {
122+
delete(key.toJS);
123+
}
124+
}
125+
126+
/// See [Map.containsKey].
127+
bool containsKey(Object? key) =>
128+
key is String && propertyIsEnumerable(key.toJS);
129+
130+
/// See [Map.containsValue].
131+
bool containsValue(Object? value) => values.any((actual) => actual == value);
132+
133+
/// See [Map.forEach].
134+
void forEach(void action(String key, V value)) {
135+
for (var (key, value) in pairs) {
136+
action(key, value);
137+
}
138+
}
139+
140+
/// See [Map.map].
141+
Map<K2, V2> map<K2, V2>(MapEntry<K2, V2> convert(String key, V value)) =>
142+
Map.fromEntries(pairs.map((pair) => convert(pair.$1, pair.$2)));
143+
144+
/// See [Map.putIfAbsent].
145+
V putIfAbsent(String key, V ifAbsent()) {
146+
if (containsKey(key)) return this[key]!;
147+
var result = ifAbsent();
148+
this[key] = result;
149+
return result;
150+
}
151+
152+
/// See [Map.remove].
153+
V? remove(Object? key) {
154+
if (!containsKey(key)) return null;
155+
var value = this[key];
156+
delete((key as String).toJS);
157+
return value;
158+
}
159+
160+
/// See [Map.removeWhere].
161+
void removeWhere(bool test(String key, V value)) {
162+
for (var (key, value) in pairs) {
163+
if (test(key, value)) delete(key.toJS);
164+
}
165+
}
166+
167+
/// See [Map.update].
168+
V update(String key, V update(V value), {V ifAbsent()?}) {
169+
if (containsKey(key)) {
170+
return this[key] = update(this[key]!);
171+
} else if (ifAbsent == null) {
172+
throw new ArgumentError("ifAbsent must be passed if the key is absent.");
173+
} else {
174+
return this[key] = ifAbsent();
175+
}
176+
}
177+
178+
/// See [Map.updateAll].
179+
void updateAll(V update(String key, V value)) {
180+
for (var (key, value) in pairs) {
181+
this[key] = update(key, value);
182+
}
183+
}
184+
185+
/// See [Map.operator[]].
186+
V? operator [](Object? key) =>
187+
key is String ? getProperty(key.toJS) as V? : null;
188+
189+
/// See [Map.operator[]=].
190+
void operator []=(String key, V value) => setProperty(key.toJS, value);
191+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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` [interop.JSObject]
47+
/// type meant to be used when the names of properties or methods are not known
48+
/// statically.
49+
extension JSObjectUnsafeExtension on JSObject {
50+
/// See [`Object.entries()`].
51+
///
52+
/// [`Object.entries()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
53+
List<(String, JSAny?)> get entries => [
54+
for (var entry in _entries(this).toDart)
55+
((entry[0] as JSString).toDart, entry[1]),
56+
];
57+
58+
/// See [`Reflect.ownKeys()`].
59+
///
60+
/// The return value contains only [JSString]s and [JSSymbol]s.
61+
///
62+
/// [`Reflect.ownKeys()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/ownKeys
63+
List<JSAny> get ownKeys => _ownKeys(this).toDart;
64+
65+
/// See [`Object.getOwnPropertyNames()`].
66+
///
67+
/// [`Object.getOwnPropertyNames()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames
68+
List<JSString> get ownPropertyNames => _getOwnPropertyNames(this).toDart;
69+
70+
/// See [`Object.getOwnPropertySymbols()`].
71+
///
72+
/// [`Object.getOwnPropertySymbols()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols
73+
List<JSSymbol> get ownPropertySymbols => _getOwnPropertySymbols(this).toDart;
74+
75+
/// See [`Object.keys()`].
76+
///
77+
/// [`Object.keys()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
78+
List<JSString> get keys => _keys(this).toDart;
79+
80+
/// See [`Object.values()`].
81+
///
82+
/// [`Object.values()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
83+
List<JSAny?> get values => _values(this).toDart;
84+
85+
/// See [`Object.assign()`].
86+
///
87+
/// [`Object.assign()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
88+
void assign([
89+
JSObject? source1,
90+
JSObject? source2,
91+
JSObject? source3,
92+
JSObject? source4,
93+
]) => _assign(this, source1, source2, source3, source4);
94+
95+
/// See [`Object.freeze()`].
96+
///
97+
/// [`Object.freeze()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
98+
void freeze() => _freeze(this);
99+
100+
/// See [`Reflect.get()`].
101+
///
102+
/// The [name] must be a [JSString] or a [JSSymbol].
103+
///
104+
/// [`Reflect.get()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
105+
R getPropertyWithThis<R extends JSAny?>(JSAny name, JSAny? thisArg) =>
106+
_get(this, name, thisArg) as R;
107+
108+
/// See [`Object.hasOwn()`].
109+
///
110+
/// The [name] must be a [JSString] or a [JSSymbol].
111+
///
112+
/// [`Object.hasOwn()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn
113+
bool hasOwnProperty(JSAny name) => _hasOwn(this, name);
114+
115+
/// See [`Reflect.set()`].
116+
///
117+
/// The [name] must be a [JSString] or a [JSSymbol].
118+
///
119+
/// [`Reflect.set()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
120+
bool setPropertyWithThis(JSAny name, JSAny? thisArg, JSAny? value) =>
121+
_set(this, name, value, thisArg);
122+
123+
/// See [`Object.isPrototypeOf()`].
124+
///
125+
/// [`Object.isPrototypeOf()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf
126+
external bool isPrototypeOf(JSObject other);
127+
128+
/// See [`Object.propertyIsEnumerable()`].
129+
///
130+
/// The [name] must be a [JSString] or a [JSSymbol].
131+
///
132+
/// [`Object.propertyIsEnumerable()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable
133+
external bool propertyIsEnumerable(JSAny name);
134+
}

js_interop/lib/unsafe.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
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+
export 'src/unsafe/object.dart';

0 commit comments

Comments
 (0)