-
Notifications
You must be signed in to change notification settings - Fork 209
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
Typed Maps - like interfaces in TypeScript #783
Comments
You can already do it by using abstract class A {
String get name;
int get age;
double? get height;
}
class B implements A {
B({this.name, this.age, this.height});
final int age;
final String name;
final double? height;
} Still it is not possible to make map to implement abstract class as |
@vanesyan that's a different thing Typed maps, aka structures/records, have a very different assignment behavior. |
The "map" in TypeScript is different because it's a class, not a map. There's a class Person {
final String name;
final int age;
final double height;
const Person({
this.name,
this.age,
this.height,
});
}
final Person person = Person(
name: 'Max',
age: 26
); Introducing this basically means mixing two completely different concepts. |
@KoTTi97 it's not maps you want, in typescript / javascript, these are called object literals. I would love to see this represented in dart as well via anonymous classes, the main use case for me is simpler json encodeing/decoding. In Go they are quite convenient as well as they remove a lot of boilerplate code that is created soley for sending / recieving responses. car := struct {
make string
model string
mileage int
}{
make: "Ford",
model: "Taurus",
mileage: 200000,
} Whats missing in dart is a way to declare a type inline as opposed to using a class declaration. The typescript example you showed doesn't really buy you anything when compared to what dart can already do, when this becomes useful is when declaring generic types, function return types and parameters, also variable types const someFunc = (user: {name: string, age: number}): {data: {didSucceed: boolean } } => {
const data: { didSucceed: boolean } = methodCall()
return {data}
} In dart you would have to define a class for every type that was defined inline here. It would be nice to have something similar in dart, but - It will probably never happen - Typescript and Go are structurally typed, I think Data classes are a great alternative and will alleviate most if not all of the pain in situations where anonymous objects would be used. |
This looks something like Java's anonymous classes. In Dart, that would probably look like: var foo = new Object() {
int something = 42;
String message = "Banana";
}; It won't give you any type for it, you'll still have to declare an interface: abstract class Foo {
abstract final int something;
abstract final String message;
}
...
var foo = const Foo() {
final int something = 42;
final String message = "some text";
}; Then |
Anonymous classes aren't the same thing imo. Typed maps/structures/records are a mechanism separate from classes. They don't have constructors/inheritance. |
I'm not sure that buys you anything over data classes for simple objects that are just containers for a set of fields with different types. It might be useful if you are actually implementing an interface's methods though. abstract class Foo {
Future<bool> doSomething();
}
var foo = const Foo() {
Future<bool> doSomething() async {
return false;
}
} I can't really think of a use case for this off the top of my head, but F# has them so I guess they are a useful somehow? Seeing as how Dart has a nominal type system, I'm not sure how anonymous objects with structural equality could be supported (aside from some from of runtime reflection maybe? ), but for the op - If/when Dart lands support for Type Aliases and Sum Types , you can (almost) solve the same set of problems in a different way, E.G. enum SomeUnionType {
OneType,
AnotherType,
LastType,
}
typedef MyType = Map<String, SomeUnionType>;
MyType myInstance = { "key1": OneType(), "key2": AnotherType(), "key3": LastType() };
// destructuring based on key name would help also if this feature is added.
var { key1, key2, key3 } = myInstance;
|
Dart has structural types too: Function types, |
I'm not a language designer, so I am not so sure about the implementation. In typescript, the type doesn't exist at runtime anyway so they can just do whatever. For Go, I'm not sure - the struct cant be assigned to interface{} unless it's a pointer. In Scala the type is assignable to Object, but then runtime reflection is used for field access, which obviously won't work for dart. |
I miss this too. interface Foo {
String bar;
}
// example 1
var baz = <String, dynamic>{
'bar': 'hi'
};
var foo = baz as Foo;
print(foo.bar); // prints hi
// the above would be basically a de-sugar to print(foo['bar']);
// example 2
var baz = Foo()..bar = 'hello';
// desugering to
var baz = <String, dynamic>{
'bar': 'hello'
}; |
I guess this kinda exists when you use JS interop: @JS()
@anonymous
class Foo {
external String get bar;
external set bar(String n);
external factory Foo({String bar});
}
var foo = Foo(bar: 'hi');
// I guess the above line transpiles to something like in JS:
let foo = {
'bar': 'hi'
}; |
For decoding where you have to change values a lot, putting them in a map where each key is a string makes more sens than to create a class for each return value. The problem is that the compiler doesn't know which key the map contains. I think this is what people would like. That the compiler knows which key the map contains without having to explicitly tell the compiler (via a class or alternatively enums for the keys) . This would be super convenient for some people. |
If you can specify the name and type of each entry in the "map" (and you need to if you're going to have any kind of static typing), that sounds like all the information needed for declaring a class. class Person(final String name, int age, [num? height]); to declare that class, then using a map-like construct doesn't seem particularly enticing any more. |
This proposal seems very much in line with how Typescript does it: It also based on structural typing instead of nominal typing. typedef Person = {String name, int age, num? height};
const Person person = (
name: "Max",
age: 26,
height: null,
); |
That's awesome. Bonus point because I assume it would be easily serializable with json. I thought I needed meta programing or data class but this could fit the bill imo |
Yes, exactly, I didn't see anything about json serializing in the proposal, but I guess it could technically be done, and could also be faster than it is now if done with records. |
the Records proposal along with meta programming proposal would rock Dart world |
Yeah I hope it doesn't take too long.
The properties have to be accessible without string like so It would be easy to serialize to json:
|
The proposal mentions the static method namedFields: abstract class Record {
static Iterable<Object?> positionalFields(Record record);
static Map<Symbol, Object?> namedFields(Record record);
} However, because it uses Symbol, it can not be used to serialize without reflection, see: There is also discussion if namedFields should be exposed at all without reflection: I hope there is some solution for this possible, I think it would be great if there is something possible in Dart that lets you serialize and deserialize fields, that is:
|
Structural typing would make a huge difference for interoperability with external systems. For example, we are working with a GraphQL API which is structurally typed. Some parts of our code work with a So, because we effectively want structural typing, we hack around it by explicitly implementing a bunch of interfaces in our generated code. In TypeScript, this is implemented as a |
is this on the radar? |
https://github.com/dart-lang/language/blob/master/working/1426-extension-types/feature-specification-views.md I think this proposal essentially covers this use case? cc @eernstg |
is this the issue in the language project funnel? #1474 |
Looks like it to me :) |
I think there are several topics in this issue. I'll say something about a couple of them. Let's consider the original example: interface IPerson {
String name;
int age;
height? double;
}
Map<IPerson> person = {
name: "Max",
age: 26,
} We don't (yet) have interface declarations in Dart, but the would presumably be a special variant of classes that support abstract class IPerson {
abstract String name;
abstract int age;
abstract height? double;
// And we could add a private constructor to prevent
// `extends IPerson` in another library.
}
In particular, we'd use plain identifiers to denote each key, requiring that is the name of a member of We could do all these things, but there are many missing pieces of the puzzle: Should We could also ask whether We could ask whether We could ask whether there would be a structural subtype relationship, that is, My take on this is that we could go down this path, but it does involve a lot of detailed language design (and possibly a large amount of implementation work in order to handle new kinds of subtyping relationships), and it's not obvious to me that it is a good fit for the language: Dart objects are not maps, and it would be a huge change to make them so. (I think that's a feature, not a bug. ;-) However, we could turn this around and consider a possible underlying feature request which is much more compatible with Dart. Let's reconsider the example here and assume that we have enhanced default constructors: class Person {
final String name;
final int age;
final double? height;
}
final Person person = Person(
name: 'Max',
age: 26,
); This doesn't involve anonymous types, or structural subtyping, but it does allow for the declaration of a class and construction of instances based on a syntax which is similarly concise as the original example, and it preserves full static typing in a completely straightforward manner. If the point is, instead, that we want to treat certain maps safely as certain existing class types then it is indeed possible to use a abstract class IPerson {
abstract String name;
abstract int age;
abstract height? double;
}
view IPersonMap on Map<Symbol, Object?> implements IPerson {
String get name => this[#name] as String;
set name(String value) => this[#name] = value;
// and similarly for `age` and `double`.
}
void main() {
Map<Symbol, Object?> map = { #name: 'Max', #age: 26 };
IPersonMap p = map;
// `p` is now treated statically safely, with the same interface as an `IPerson`.
print(p.name);
p.name = 'Joe';
} The view is a static mechanism (and not yet part of the language), but it includes a This is of course a lot more verbose, and the purpose is completely different: This mechanism is aimed at supporting a manually specified statically safe treatment of objects of a freely chosen underlying implementation type. The point is that the implementation type ( |
I can see a builder to make it easy to JSON interop. Would the below work? // we take an abstract class and add the Interface annotation
@Interface()
abstract class Person {
abstract String name;
abstract int age;
abstract height? double;
}
// generate the view Interface through the builder
view PersonInterface on Map<String, Object?> implements Person {
String get name => this['name'] as String;
set name(String value) => this['name'] = value;
// and similarly for `age` and `double`.
}
// declare some API bindings
class SomeApi {
// expose the Person endpoint
Future<Person> fetchPerson() async {
// fetch some map
final resultMap = await ajaxSomePersonMap();
// return as the Person interface
return resultMap as PersonInterface;
}
}
Future<void> main() async {
final api = SomeApi();
// call the API in a object oriented way
final person = await api.fetchPerson();
// typed =]
print(person.name);
} This along with static meta programming can finally make working with json easier and satisfying. Question: I understood from the proposal that |
@jodinathan, we'd need a couple of adjustments: @Interface()
abstract class Person {...} // Same as before.
view PersonInterface on Map<String, Object?> implements Person {...} // Same as before.
class SomeApi {
Future<Person> fetchPerson() async {
final resultMap = await ajaxSomePersonMap();
return resultMap.box;
}
}
void someOtherFunction(Person p) {...}
Future<void> main() async {
final api = SomeApi();
final person = await api.fetchPerson();
// Statically typed; `person` has type `Person`.
print(person.name);
someOtherFunction(person);
} This would work, and the object which is the value of You could also maintain a more lightweight approach where the map isn't wrapped in a different object. In this case all instance method invocations would be static (that is, there is no OO dispatch and they could be inlined), but in return you must maintain the information that this is actually a But you still have the @Interface()
abstract class Person {...} // Same as before.
view PersonInterface on Map<String, Object?> implements Person {...} // Same as before.
void someOtherFunction(Person p) {...}
class SomeApi {
Future<PersonInterface> fetchPerson() async => await ajaxSomePersonMap();
}
Future<void> main() async {
final api = SomeApi();
final person = await api.fetchPerson();
// Statically typed; `person` has type `PersonInterface`.
print(person.name);
// You can still use `person` as a `Person`, but then it must be boxed.
someOtherFunction(person.box); // Just passing `person` is a compile-time error.
} The point is that you can pass the |
in the json use case, the view is enough. can we pass the interface around? view PersonInterface on Map<String, Object?> implements Person {...} // Same as before.
void someOtherFunction(PersonInterface p) {
print(p.name);
}
Future<void> main() async {
final api = SomeApi();
final person = await api.fetchPerson();
someOtherFunction(person);
} |
Yes, inside |
@eernstg this is awesome! We really need this 🥺 |
Any progress? Typescript is amazing in this regard. |
The lack of this feature is error prone when adding a property to an serializable class. The fromMap will fail to compile while the toMap won't. Hopefully macros will help there. |
Records feature is kind of this. |
any progress? |
This is done already.. |
It is very, very unlikely that Dart will introduce a "map type" which you can check whether an existing normal map "implements". For having simple structures with typed fields, Records is probably the most direct implementation of what's being asked for here, and they exist since Dart 3.0. The next best thing would be "data classes", which could be defined something like my example above. The one thing that's not being asked for, but is probably wanted, is JSON encoding and decoding. The value will not be a Still, it's not completely impossible. It's just not particularly useful. I don't see anything left to do here, which is at all likely, except possibly simple data classes. |
Macros could also add a form of support to records too fwiw, through generated helper methods to do the conversion. |
see pattern matching if (map case {'name': String name, 'age': int age}) {
print('name: $name, age: $age');
} |
also, records could support json encoding with macros too. |
I didn't quite understand the problem ... isn't that what it's already saying? typedef TPerson = ({ String name, int age });
TPerson person = (name: "teste", age: 10); |
No, because you lack person.values, keys, serialization and any functionality that expects a map |
Perhaps you want a map extension type? extension type YourType(Map<String, dynamic> map) {
String get thing => map['thing'] as String;
int get i => map['i'] as int;
} |
Google (as well as many others) is using Zod to type a scheme for Gemini, so the model knows what it can return. When value is returned, it is already cast in the schema you provided. That's basically impossible in Dart right now, but this issue would allow it. |
Sounds like you'd want a Zod-to-object conversion. Or Zod-to-record. Typed maps are an oxymoron. If it's a Dart It "works" in JavaScript/TypeScript because the type system isn't sound or safe. If you want unsound types in Dart, you need to add runtime type checks. The map itself cannot do that, so you need something around the map. Maybe an extension type. |
I support this question. Since Dart maps are used everywhere (if not by you, then in some library), it would be very helpful to have some tool for map typing. |
A tool for map typing is not necessarily the same as typed maps. A type map would be a type that is a subtype of Rather than introduce an unsound type like extension type FooBar(Map<String, Object?> _)
implements Map<String, Object?> {
int get foo=>_["foo"] as int;
set foo(int v) { _["foo"]=v;}
String? get bar =>_["bar"] as String?;
set bar(String? v) { _["bar"]=v;}
} It's more writing, but I can make your a small script to generate this extension type from the type map above, if that's a problem. You can still look up anything using If you want to enforce a type scheme for a map, you can make a wrapper Aarons the map that checks the values coming in, and throws if they're invalid. But you can't have dependent typing in Dart. Not any time soon, at least. That's a very big mouthful, and unlikely to be worth the very significant effort to make it sound. (If that's even possible.) |
Rather than defining an extension, you can define a map-backed class. I played with this idea years ago - see https://github.com/tatumizer/pigeon_map As a bonus, you get a much more efficient implementation (both in terms of performance and memory footprint) of Map. There are other advantages (e.g. the ease of serialization). (*) Pigeon Map is my Opus Magnum, which acquired the agency and actively resisted my attempts to remove it from the Repository. |
I would love to see typed Maps in Dart like it is possible in TypeScript.
TypeScript example:
I wish Dart would support something like that so i do not have to create a class for it. It could look something like this:
The text was updated successfully, but these errors were encountered: