dart-lang / language

Design of the Dart language

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Type aliases: Typedefs for non-function types

mit-mit opened this issue Β· comments

Realizes #66, Feature specification

Sample typedef & usage:

typedef IntList = List<int>;
IntList il = [1,2,3];

Related issues: dart-lang/sdk#2626

Feature specification proposal in this PR.

Said feature specification landed as efdc2fa.

PR created for the feature specification: One rule needs to be discussed, and the right thing to do is to say explicitly that no conclusion has been reached yet, with respect to that particular rule.

I opened an issue for discussion of the question of using typedefs where classes are expected here.

Said PR now landed.

I think we need to update the spec to reflect the consensus on static method access.

Landed #116 for that.

I perused the spec, but I didn't see an obvious answer. How will the analyzer handle this?

typedef IntList = List<int>;
IntList il = [1,2,3];
List<int> li = il; // potentially problematic

Presumably that would be either a warning or an error, correct? What about this one (building on the code above)?

void foo(IntList a) {}
List<int> b = [1,2,3];
foo(b); // potentially problematic

In this case it isn't clear that b is intended to be an actual IntList, so allowing the call to foo might introduce a bug. On the other hand, I suspect that quite a few people would want to do this without an explicit cast for the sake of convenience.

List<int> and IntList is the same type, just different spelling, so there should not be a problem with this example: Inference should make [1,2,3] mean <int>[1,2,3] and the initialization of li is fine. Same situation with foo(b).

But someone might change the definition of IntList, and if the piece of data in question was never really intended to follow IntList in the first place, a call like the second one in my comment shouldn't be allowed in that case. In other words, everything might type check accidentally.

For example, the second call to foo here won't compile because I've declared b to be an int, but not necessarily a MyInt.

package main

type MyInt int

func foo(a MyInt) {
    println(a)
}

func main() {
    var a MyInt = 0
    foo(a)
    var b int = 0
    foo(b)
}

That's not the same thing.
This feature doesn't create a new type, it just makes an alias.

typedef Foo = int;

void main() {
  print(int == Foo); // true
}

OK, that's really what I was curious about, I suppose. Thanks for clarifying!

Would this include:

typedef Test<T> = T Function();

typedef Test2 = Test<int>;

?

Currently, this code does not compile, for no obvious reason. This is very limiting if we have anything more complex than T Function().

The 'typedefs for non-function types' feature has not yet been implemented. It will happen, but the efforts to support other features (especially extension methods and non-nullable types) have left no resources available for this feature, so it's been postponed for a while.

This means that we only have the kind of type alias that does not support non-function types: The grammar rule requires a <functionType> on the right hand side of =.

That makes typedef Test2 = Test<int> a syntax error (actually, no tool is required to tell which compile-time errors are syntax errors and which ones are "other" compile-time errors, but it must be a compile-time error of some kind).

When this feature is implemented it will certainly be possible to have declarations like typedef Test2 = Test<int>;.

In any case, there should not be any particular limits on the complexity of the types that you define a given type alias to denote (neither with the current type alias feature, nor with the generalization that supports = <type> rather than = <functionType>).

One question about this proposal:
If I define an extension function on a type alias, it will be available to the 'aliased' type? Like:

typedef IntList = List<int>;

extension on IntList {
   doSometing();
}

var listOfItens = [1,2,3];

listOfItens.doSomething(); <-- It will be available?

That's how it works already with extensions on functions, so likely.

typedef VoidCallback = void Function();

extension on VoidCallback {
  void foo() {}
}

void main() {
  void Function() test = () {};

  test.foo();
}

That's how it works already with extensions on functions, so likely.

typedef VoidCallback = void Function();

extension on VoidCallback {
  void foo() {}
}

void main() {
  void Function() test = () {};

  test.foo();
}

Wow thats bad to hear 😒
If it was not available, we could create fake class inheritance, giving us some interesting possibilities. =/

But that's not a typedef anymore. That's a new type
Your request may be part of #546.

Hello! Any news on this feature? Also, Merry Christmas to everyone in advanced ;)

commented

I think lots of devs waiting for replacing Map<String, dynamic> with JsonObject πŸ˜„

typedef JsonObject = Map<String, dynamic>

Just

type JsonObject = Map<String, dynamic>

I think lots of devs waiting for replacing Map<String, dynamic> with JsonObject πŸ˜„

typedef JsonObject = Map<String, dynamic>

true.
fully typing everytime Map<String, dynamic> is such a pain

Many devs are also waiting for union typedefs 😁

@LuisDev99 wrote:

Any news on this feature?

Passing --enable-experiment=nonfunction-type-aliases on a bleeding-edge compiler (esp. dart2js) should allow you to verify that something is going on (most likely: it crashes). It's not working, but it's moving, which is an improvement over the situation a few months back. And Merry Holidays to all, of course! ;-)

How would i use --enable-experiment=nonfunction-type-aliases on Flutter?

@zl910627 wrote:

How would i use --enable-experiment=nonfunction-type-aliases on Flutter?

Flutter has its own version of each tool, so in general you may not be able to get the same features as you do with the plain Dart tool chain. It is probably not worth the trouble to try to cheat Flutter into working with the bleeding edge version of a tool.

Also it would be great for use cases such as

typedef UserId = int;

You can do typedef UserId = int;, but it won't give you a new type, it gives yet another name to an existing type. We'd need some other mechanism if you want to make the new type distinct (e.g., such that int x = 1; UserId id = x; would be a compile-time error.

Also it would be great for use cases such as

typedef UserId = int;

What you are asking is nothing related with typedef's but with inline classes
Kotlin does this: https://kotlinlang.org/docs/reference/inline-classes.html
At compile time you get a representation and UserId != int, but at run-time t disappears and only int exists

Right, Kotlin inline classes are similar to Haskell newtype in a couple of ways, and they are both intended to provide a new (assignment incompatible) type, with zero representation overhead at run time.

(Kotlin inline classes don't actually guarantee the zero cost property in all situations, because there will be a wrapper object at run time in the case where an expression whose type is an inline class is used where some other type is required, cf. lines where 'boxed' occurs here.)

We have discussed similar mechanisms for Dart, under names like 'views' or 'view classes', or 'extension types'.

Hello. This feature was requested 8 years ago. I'm especially interested in typedef Foo = int; because it makes code much cleaner (practiced it in web development). Are you going to implement it and release it? Do you have any dates/timelines and so on?

Until December 2019 we did not work on it. I did some work on it before the holidays, and I just restarted working on it yesterday. (Most of the time is taken by null-safety issues these days.) So there's hope. ;-)

Nice! Any timelines for either of them (doesn't have to be official, just a general idea of what's going on)?

Wow! ;-)

Bleeding edge plus https://dart-review.googlesource.com/c/sdk/+/141240 runs 35 out of 37 test files (containing about 500 different usages). But one non-trivial one is still missing, e.g.:

typedef T<X, Y> = Map<Y, List<X>>;

void main() {
  Map<int, Iterable<String>> xs = T.fromIterables([], []);
}

Inference should complete this the initializing expression to T<String, int>.fromIterables(<int>[], <List<String>>[]), but that is currently not inferred (it just pretends that there is no context type, and uses default type arguments).

Oh, and null-safety is the majority of everybody's work, on the whole team.

Important caveat: The ongoing work on non-function type aliases targets the common front end. A similar effort is needed with the analyzer, and then there are some additional targets like the IntelliJ plugin, github code highlighting, etc. Things take time..

commented

What if we could use C++ preprocessor inside dart.

#define JMap Map<String, dynamic>
#define UserId int

Using msbuild we can run preprocessor by: cl /P file.dart. But I don't know how to automate this.

Note that the preprocessor approach could allow for passing type arguments from one type alias to another (using a non-standard syntax for passing type arguments), but there would be no support for performing inference on terms using a type alias (it's just a preprocessing-time error to omit the type arguments).

I suppose all the work being done is to get Dart (and the analyzer) to recognize an alias as an official type, but honestly for now, I would settle for just a pre-processing script that copy-and-paste the new type before passing it to the analyzer or compiler (even if the alias name won't show up).

I needed this quite some time ago and now it just happened how good it would be if Dart had it back then.

I've figured out that Map<String, Object> suites better for my project instead of Map<String, dynamic>.
I may be wrong, but it seems to be less error prone and also Object can have extension methods.

This changing would be easier if I had a Pair typedef

Any news on this feature? Any timelines?

I'd really appreciate being able to use it.
Is there anything we could do to help?

I prefer using typedef instead of #define, solely because the # looks ugly on a language that doesn't heavily use # besides C/C++. But also because it reminds me of how evil macros are

@rhbrunetto wrote:

Any news on this feature? Any timelines?

Several bugs fixed since this comment, still missing: Inference.

@eernstg are you implementing the whole thing by yourself?

Yes. Everybody (including me) is mainly working on null-safety, but we wanted to get non-function type aliases moving as well, so I started working on it. Hope to make some progress this week again. ;)

Thanks @eernstg! We all appreciate this work!

@eernstg Glad to see this is being implemented!

I'll mention a use case that nobody noted as yet: since Dart's FFI doesn't (dart-lang/sdk#36140) have any support for C's standard variable-sized integer types (int, long, size_t, etc.), it would be tremendously helpful to be able to conditionally (at compile time) define type aliases to the effect of, for example, CLong being a typedef for either Int32 or Int64.

From the discussion here so far, I understand that the right-hand side of a typedef probably won't support a conditional expression in the initial implementation. Perhaps that might be possible to work around with using conditional imports to enable the FFI use case?

Now that NNBD is in technical previews, is this at the top of the figurative to-do list?

Now that NNBD is in technical previews, is this at the top of the figurative to-do list?

This is already being implemented, just be patient.

Now that NNBD is in technical previews, is this at the top of the figurative to-do list?

This is already being implemented, just be patient.

I understand, but @eernstg had noted earlier in the issue that the main priority of the Dart team was NNBD and related work. I was just asking if some of that time has been freed up now.

Now that NNBD is in technical previews, is this at the top of the figurative to-do list?

This is already being implemented, just be patient.

I understand, but @eernstg had noted earlier in the issue that the main priority of the Dart team was NNBD and related work. I was just asking if some of that time has been freed up now.

Well, NNBD is at an advanced stage, IIRC its expected to be released in a few months. I don't know if this feature will be ready until then, but I'm sure that if not, as soon as NNBD is released @eernstg will have more time to work on this, so I don't expect this to delay much.

Great to see enthusiasm for this feature, it's picked up quite a few upvotes since I last checked it! We're definitely on the downhill track for null safety but we're still pretty heavily in the thick of rolling that out. I don't think we can make any promises about timeline for this feature, but we are still cautiously optimistic that we'll be able to get back to it soon. Thanks for the patience.

Newcomer to Dart & Flutter here, still learning the ropes.
I was pretty amazed when I found out I could not do something as simple (and essential) as :

typedef JSONObj = Map<String, dynamic>;

I'm using this sort of thing regularly e.g., in my Swift code.

Just upvoted this - let's hope it gets implemented soon.

commented

Great to see enthusiasm for this feature, it's picked up quite a few upvotes since I last checked it! We're definitely on the downhill track for null safety but we're still pretty heavily in the thick of rolling that out. I don't think we can make any promises about timeline for this feature, but we are still cautiously optimistic that we'll be able to get back to it soon. Thanks for the patience.

Any more updates @leafpetersen ?

I hate to be vague, but I also hate to dangle promises that I'm not sure I can keep. Sorry! :} This is still pretty much the top of the queue of small features that we'd like to ship soon, but we don't have a concrete ship date yet.

Still no concrete shipping date, but the analyzer implementation is complete (thanks, @scheglov!). Still missing is support in IntelliJ, dartfmt, and several other tools. But we're definitely getting closer.

commented

If I had this, then I would write

typedef string = String 

or

typedef Int = int
typedef Double = double
typedef Bool = bool

in the first place

make the dart language consistent in naming case

This is different from Java because in Java int is the base type and String is the object type, so it makes sense to be case-sensitive, but this is not the case with Dart, which only has a naming difference

typedef IntList = List<int>;
IntList il = [1,2,3];

To be honest, I think that is the worst possible example πŸ˜‚

One of the more popular examples would be JSON:

typedef Json = Map<String, dynamic>;

class User {
  final String name;
  final int age;

  User.fromJson(Json json) : 
    name = json ["name"],
    age = json ["age"];
  
  Json get json => {
    "name": name, 
    "age": age,
  };
}

@eernstg

Still no concrete shipping date, but the analyzer implementation is complete

Wait, if the compiler and analyzer both support this, can it theoretically be partially released now with proper tooling later?

commented
typedef IntList = List<int>;
IntList il = [1,2,3];

To be honest, I think that is the worst possible example πŸ˜‚

I remember when I write pascal I had to use StringList because it doesn't supported templates (aka: generics).

commented

One of the more popular examples would be JSON:

typedef Json = Map<String, dynamic>;

class User {
  final String name;
  final int age;

  User.fromJson(Json json) : 
    name = json ["name"],
    age = json ["age"];
  
  Json get json => {
    "name": name, 
    "age": age,
  };
}

Since json objects can be also arrays and dart doesn't supports union types like List<dynamic> | Map<String, dynamic> why we're not using dynamic for storing json data? Don't take it serious, i'm myself using Map<String, dynamic> and using DTOs on higher level layers.

You're right, in other instances I do use Lists. But the way I tend to structure my data, everything is a class in a Map, or a list of such maps. So when I have a list, it usually ends up being passed into User.fromJson one by one, hence the Maps everywhere. Personally, I feel that types are not a big issue when it comes to JSON, since it's either Map<String, dynamic> or List<dynamic>. Once you determine if it's a list or a map, you're pretty much done.

Also, am I the only one who does

Json get json => { };

instead of

Json toJson() => { };

It feels like every example I see has the latter, but I find getters to be so much cleaner.

You're right, in other instances I do use Lists. But the way I tend to structure my data, everything is a class in a Map, or a list of such maps. So when I have a list, it usually ends up being passed into User.fromJson one by one, hence the Maps everywhere. Personally, I feel that types are not a big issue when it comes to JSON, since it's either Map<String, dynamic> or List<dynamic>. Once you determine if it's a list or a map, you're pretty much done.

Also, am I the only one who does

Json get json => { };

instead of

Json toJson() => { };

It feels like every example I see has the latter, but I find getters to be so much cleaner.

I guess it is common to set a local var to function calls while a getter is often called multiple times.

commented

You can also write something like that if you want to sacrifice code cleanness for 1ms performance improvement:

Json _json;
Json get json => _json ??= <String, dynamic>{
  ...
};

It will cache json value in _json field which will not require rebuilding Map on each usage.

This feature is really near to completion now. A useful example could be a type alias that encapsulates a certain regularity in the use of an existing type:

typedef MapToList<X> = Map<X, List<X>>;

void main() {
  MapToList<int> m = {};
  m[7] = [7];
  m[8] = [2, 2, 2];
  for (var x in m.keys) {
    print('$x --> ${m[x]}');
  }
}

The point is that MapToList<X> allows us to specify int just once, and then the standard form Map<int, List<int>> is implied, and that will compute the second type argument to the map literal {} implicitly.

Thanks for all your hard work on this feature! Dart is special in that it pays so much attention to the little things.

A wild commit appeared yesterday! It seems this will be released with Dart 2.13!

https://dart-review.googlesource.com/c/sdk/+/192948

Thanks for that!

There's a great example in the CHANGELOG that is probably worth repeating -- deprecating classes.

Let's say you have this:

class BadName { /* some methods */ }
class OtherLibraries extends BadName { }

So you want to rename BadName, but that's a breaking change since other people depend on them. Well, you can safely deprecate it by doing this:

class BetterName { /* some methods */ }  // changed the original

@Deprecated("Use BetterName instead")
typedef BadName = BetterName;

Now, all classes that extend BadName will cotinue to work, but will show a warning. When it's time for the breaking change, simply remove the typedef and keep everything else the same.

A wild commit appeared yesterday! It seems this will be released with Dart 2.13!

https://dart-review.googlesource.com/c/sdk/+/192948

Heh - can't keep anything from you guys. :) Just to manage expectations a bit... I haven't landed that patch yet, and there's still a non-zero (but hopefully small) chance that we won't make the branch cut date for 2.13.

It does not change a lot when it will be released in the near future in my mind @leafpetersen :)

Considering that the feature has been finished in most places (according to dart-lang/sdk#44951) and the fact that we should be aware of the standard release cycle, we should not have any different expectations in the first place ;)

Why this issue stills open? 😁😁😁

You're right, done!

Now that I actually want to use this feature (with JSON types, surprise surprise), I came across a problem: Where do I actually put the typedef?

In my Flutter app, I have this file structure:

|- lib
   |- data.dart -- exports everything in src/data
   |- src 
      |- data
         |- a.dart
         |- b.dart
         |- c.dart

If I want classes A, B, and C, each in their respective files, where should I put the Json alias? Seems like a lot of overhead to add a json_alias.dart file just for that. If I put it in each of the data files, then there will be conflicts. And I don't want to put it in any one data file, since Json isn't intrinsic to any specific class. If I put it in data.dart, then all importers can see it, but none of the data files can use it in their definitions!

@eernstg, thoughts? Are there any tests that happen to import aliases that I can pull inspiration from?

Seems like a lot of overhead to add a json_alias.dart file just for that.

I would probably do this.

Okay, thanks for the recommendation, I'll do that.

@Levi-Lesches
Now that I actually want to use this feature (with JSON types, surprise surprise), I came across a problem: Where do I actually put the typedef?

If you will be using more typedefs, a suggestion would be an encompassing root-level typedefs.dart; it doesn't need to be exported.

I have seen a lot of packages that would, in your scenario, have a src/data/common.dart solution, importing common into each a, b, and c.

Folks, this is all very interesting but Github bug comments are not the right space for it.

commented

I tried this feature in IntelliJ dart plugin , like it still not support.

@fanthus, if you wish to report an issue with the IntelliJ plugin and non-function type aliases, please create an issue with 'area-intellij' and give more details.

Can it be possible to get the name of typedef instead the underlying runtimeType in future? Since it's just an alias, I think it might be feasible with an extra field for variables with typedefs.

typedef Meaning = int;

final Meaning life = 42;

void main() {
  print(life.runtimeType); // prints 'int'
  print(life.typedef); // prints 'Meaning'
}

Why I would need this?
Most of the time, I use runtimeType for labeling things in logs. But for typedefs it's not a case obviously. Example:

typedef SessionID = String;
final provider = Provider<SessionID>();

runtimeType is Provider<String> but there could be way to print Provider<SessionID> which is much more identifiable.
Also it would be very beneficial for writing something like golang style type-switch.

@esenmx The way you describe it, it is a new type rather than an alias. I guess the problem is that extends String does not work, but it is not an alias if you can switch on it.