Skip to content

Remove dynamic type #3192

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
maeddin opened this issue Jul 4, 2023 · 65 comments
Open

Remove dynamic type #3192

maeddin opened this issue Jul 4, 2023 · 65 comments
Labels
request Requests to resolve a particular developer problem

Comments

@maeddin
Copy link

maeddin commented Jul 4, 2023

Proposal

Dart should remove the dynamic type for stricter type-safety, better performance and less support for bad practices.

Justification

The use of dynamic is a bad practice, often even referred to as such in the documentation:

However, there are ways to bypass dynamic calls and implicit casts for your own projects using lints (e.g. with strong-mode).
But this does not solve the problems that the support for dynamic has for the Dart language in general.
Firstly, it results in worse performance of the compiled code, since more inefficient runtime checks are required. (I'm not an expert on Dart compilers, so correct me if I'm wrong on this point).
Secondly, the support of dynamic prevents other features that many developers would like to use in a type-safe object-oriented language. For example, private, protected and public modifiers are not possible because they could not support dynamic types except through very inefficient runtime checks (dart-lang/sdk#33383).

In general, Dart tries to be as type-safe as possible. But on the other hand there is this relict dynamic, which stands against it.
dynamic generally feels like a concession to JavaScript developers to get more of them to switch to Dart/Flutter. Developers who have moved from Java/Kotlin/Swift/C++,... are just disturbed by dynamic and it also doesn't bring any benefits except more opportunities to apply bad practices.

Impact

All code that currently uses dynamic would have to be changed to Object? or subtypes and implicit casts would be required. Also, method calls would no longer be possible without knowing the type of an object.

Mitigation

Since this would be a null-safety level change, it would have to be made optional at first and fully introduced with the next major release.

@lrhn
Copy link
Member

lrhn commented Jul 4, 2023

This is a language change request. Moving it to the language repository.

@lrhn lrhn transferred this issue from dart-lang/sdk Jul 4, 2023
@lrhn lrhn added the request Requests to resolve a particular developer problem label Jul 4, 2023
@mateusfccp
Copy link
Contributor

mateusfccp commented Jul 4, 2023

I don't use dynamic in ages. Everything can be written with Object? instead, except for maybe some sketchy edge-cases.

I see no downsides except for the fact that this is a very breaking change.

I fully support this change, but I find it hard to believe that the Dart team would go through this route, as they are pretty conservative about breaking changes.

@rrousselGit
Copy link

rrousselGit commented Jul 4, 2023

Same here. I've stopped using dynamic forever ago.

The only use-case I can think of today is mocking, which currently often relies on dynamic invocation with noSuchMethod.

There's also how dart:convert does a dynamic invocation of toJson

So removing it could be quite breaking. Although not impossible.
For instance, mockito already uses code-generation. So it could stop relying on dynamic altogether

@rubenferreira97
Copy link

I am also in favor of this change. As far as I know, the dynamic type introduces many edge cases to the language.
However, a question arises: Would removing the dynamic type from Dart pose a challenge for implementing interoperability with other languages, such as JavaScript?

@srawlins
Copy link
Member

srawlins commented Jul 4, 2023

A stepping stone to removing dynamic from the language would be to ban dynamic calls in the recommended (or core) lint rule sets. You can upvote here: dart-lang/core#750 😁

@ykmnkmi
Copy link

ykmnkmi commented Jul 5, 2023

I like it. I have been using strict type checks and explicit casting from the beginning using the Dart language.

@lrhn
Copy link
Member

lrhn commented Jul 5, 2023

Is it the dynamic type existing, which is the problem, or its accidental use?

What if the only expression that could have type dynamic was e as dynamic, effectively making as dynamic a special syntactic form with special rules for member access and assignability, but dynamic couldn't otherwise be used as a type?
Then you can still ask for a dynamic invocation if you really need it, but it absolutely certainly cannot happen by accident, or because someone else chose dynamic for you.

Dynamism would then be an opt-in feature, with a clear syntactic marker everywhere it's used.
But you'd still have the power when you really need it.

The alternative is that there are existing features which cannot be retained. Like jsonEncode trying to call toJson, which is admittedly no great loss, but still an unmitigatable breaking change.

It might encourage introducing new, simple, interfaces and force existing code to implement those, in order for existing dynamic code to keep working.
Not sure having lots of small interfaces is a win. And without interface injection, it requires total cooperation.

(Not saying we cannot remove dynamic entirely, which I would have said before patterns, but it is a distinguishing feature of Dart that it is there, as a backdoor when what you want to do isn't something you can convince the type system of. Like late and as, ways to tell the static analysis that you do in fact know better. Which you better do then. But you're not asking to remove as, which is just as unsafe as dynamic, so I'll presume it's more about performance, and then just not using dynamic should be enough. Avoiding accidental use should put that within your own control.)

@mateusfccp
Copy link
Contributor

What if the only expression that could have type dynamic was e as dynamic, effectively making as dynamic a special syntactic form with special rules for member access and assignability, but dynamic couldn't otherwise be used as a type?
Then you can still ask for a dynamic invocation if you really need it, but it absolutely certainly cannot happen by accident, or because someone else chose dynamic for you.

This would be better than what we have today and less breaking, so it may be an alternative path. From there we can think if it's really valuable to remove dynamic entirely.

But you're not asking to remove as, which is just as unsafe as dynamic, so I'll presume it's more about performance, and then just not using dynamic should be enough.

Honestly, I would also ask to remove as if we could improve our type system in the areas that today require us to use as. There are a few cases where I have to use as because I can't express what I want with the type system, and every single time I do this I become completely afraid of having runtime exceptions because I made something wrong. Yes, I am telling to the compiler that I know better, but the fact is that I am not 100% sure that I know better, it's only that I have no other way to to what I have to do.

@maeddin
Copy link
Author

maeddin commented Jul 5, 2023

Is it the dynamic type existing, which is the problem, or its accidental use?

It is even the existence of dynamic for me, but through it, of course, the accidental use of it also.

What if the only expression that could have type dynamic was e as dynamic, effectively making as dynamic a special syntactic form with special rules for member access and assignability, but dynamic couldn't otherwise be used as a type? Then you can still ask for a dynamic invocation if you really need it, but it absolutely certainly cannot happen by accident, or because someone else chose dynamic for you.

Dynamism would then be an opt-in feature, with a clear syntactic marker everywhere it's used. But you'd still have the power when you really need it.

In my opinion, dynamic could be left optional in the language, as long as there is no performance loss for the language in general in the compiled code (so also e.g. for dart2wasm) if you don't use dynamic in it explicitly. Unfortunately, I cannot assess this, since I have little idea of the concrete implementation of the compilers.
If the compromises currently made for dynamic are too much, it should be removed completely. Because this way you could achieve not only clean code but also better native performance.

The alternative is that there are existing features which cannot be retained. Like jsonEncode trying to call toJson, which is admittedly no great loss, but still an unmitigatable breaking change.

It might encourage introducing new, simple, interfaces and force existing code to implement those, in order for existing dynamic code to keep working. Not sure having lots of small interfaces is a win. And without interface injection, it requires total cooperation.

For jsonEncode, you could either introduce a simple interface or have jsonEncode only support Maps, Lists, Strings, Numbers, etc. and you would have to call the toJson() methods of your own classes yourself before passing the result into jsonEncode. Either way, this wouldn't be the most drastic change (as you have already stated).
Too many small interfaces would of course be annoying, although you need at most one interface in most cases anyway. Besides, a lot of things (currently) are done by code generation, so for most use cases there would be no need for interfaces at all (unless I'm missing something here).
However, interfaces would be a compromise that can be made for compile time type checking. In the end, using interfaces is still cleaner than calling methods like toJson() through the dynamic type.

But you're not asking to remove as, which is just as unsafe as dynamic, so I'll presume it's more about performance, and then just not using dynamic should be enough. Avoiding accidental use should put that within your own control.

as should not be removed, because you actually need casts often. In Dart this happens implicitly most of the time, but the static analysis can't find everything, so sometimes you have to do it manually. And unlike dynamic, for using as you need to know the type, on which you want to call a method.

@lrhn
Copy link
Member

lrhn commented Jul 5, 2023

as should not be removed, because you actually need casts often

You never need a cast, you can do an is check to promote instead. It's even easier now with patterns.
You then have to handle the else branch yourself, and hopefully throw a better error message than TypeError: A 'Foo' is not a 'Bar'.

That's just cumbersome, in the cases where you do know that the value will definitely have the assumed type.
But you're depending on dynamic knowledge that the compiler cannot verify, setting yourself up for runtime failure, with no static warning possible, if you make a mistake.

Implicit downcast from dynamic is just the same as an as ContextType. That's not the problematic feature. It's the dynamic invocation which cannot be simulated without knowing all possible types.

@maeddin
Copy link
Author

maeddin commented Jul 5, 2023

as should not be removed, because you actually need casts often

You never need a cast, you can do an is check to promote instead. It's even easier now with patterns. You then have to handle the else branch yourself, and hopefully throw a better error message than TypeError: A 'Foo' is not a 'Bar'.

That's just cumbersome, in the cases where you do know that the value will definitely have the assumed type. But you're depending on dynamic knowledge that the compiler cannot verify, setting yourself up for runtime failure, with no static warning possible, if you make a mistake.

Using is leads to an implicit cast. This is a somewhat nicer thing to do, but nothing different. I almost never use as myself because of is and patterns.
That was not well expressed by me, sorry.
But this issue is primarily about dynamic and not about as, which is why this discussion would probably be better suited in another issue.

@rrousselGit
Copy link

I think the idea of having dynamic be more explicit is a good one.

Changing type inference to instead rely on Object? instead of dynamic would be great.
I've done that in Freezed by changing fn<T>(...) to fn<T extends Object?>() in generated code.
This seems to tell type inference to not rely on dynamic

There are also a bunch of APIs which promote dynamic.
Like jsonDecode. It'd be nice if the SDK and Flutter stopped using it. That would push folks to do the same too.

@goderbauer
Copy link

Is it the dynamic type existing, which is the problem, or its accidental use?

For me, it is the accidental use. If you'd have to be very very explicit every time you want to do a dynamic invocation it wouldn't be a problem anymore.

It'd be nice if the SDK and Flutter stopped using it.

Wondering if you have any examples where Flutter is using dynamic. We try very hard not to and I'd be open to removing any remaining instances.

@incendial
Copy link

Wondering if you have any examples where Flutter is using dynamic. We try very hard not to and I'd be open to removing any remaining instances.

@goderbauer here is the report of avoid-dynamic on the whole Flutter repo (beta branch) report.txt. Will this work?

@rrousselGit
Copy link

Wondering if you have any examples where Flutter is using dynamic. We try very hard not to and I'd be open to removing any remaining instances.

Honestly I grouped flutter and Dart SDK together. It could be that Flutter doesn't really use it, I don't remember.

The sdk uses it quite often I think. In part with error handling or json

@lrhn
Copy link
Member

lrhn commented Jul 5, 2023

The sdk uses it quite often I think. In part with error handling or json

Mainly JSON, because JSON is inherently untyped, and Dart 1 used dynamic to mean "this cannot be typed". Also to make it easier to use. Now that the rest of the language is actually typed, removing dynamic from JSON would just be breaking.
Not that I don't want to, but it had to piggy-back on some larger related change, it's not worth the breakage by itself.

Some async error handling uses Function, but only because it has to accept two incompatible function types, not necessarily because it wants to call them dynamically. It would still work if you couldn't call something typed as Function dynamically.

But there are places in the SDK libraries where some raw type gets instantiated with dynamic, and we can't fix that because someone, somewhere, depends on it. That's just annoying.

@gintominto5329
Copy link

Could be related: dart-lang/sdk#50874

@lucavenir
Copy link

it had to piggy-back on some larger related change, it's not worth the breakage by itself.

Well, yes, I guess that when there's a breaking change, one would try to fit in good counter measures as much as possible. I guess that rarely you'd release, say, "Dart 4" just because of a single feature like this one.

If anything, I wish a potential "Dart 4" version would remove dynamic entirely, would uniform the switch syntax and thus would offer easier pattern matching syntax. Also a public / private / protected solution would be nice. And, of course, metaprogramming. I know, I am being imaginative.

But there are places in the SDK libraries where some raw type gets instantiated with dynamic, and we can't fix that because someone, somewhere, depends on it. That's just annoying.

I can't see how this is insurmountable. By definition, a breaking change un-supports ..stuff! Folks would need to adapt to it. SDK / Flutter / core libraries included.

... But I sincerely can't wait to do so! If there's a fair amount of benefits that indirectly affect or bring other features, I'd quickly transition to this. As many others, I don't use dynamic, either. And as Remi just said, it'd be nice if the whole ecosystem stopped using it. Like, to deprecate it at some point. That's a good incentive for the whole community.

@ds84182
Copy link

ds84182 commented Jul 9, 2023

I don't think dynamic should go away entirely, but it should be made safer. A form of structural typing can be introduced to make dynamic a bit more ergonomic. For example, toJson in dart:convert json encoding:

// Interface that can be explicitly or implicitly implemented by a type
// as long as it contains all matching members.
typedef interface ToJson {
  Object toJson();
}

// ... json innards in dart:convert when handling an unknown type:
// assume object is declared as `dynamic object;`.
if (object is ToJson) {
  object = object.toJson();
}

Downstream consumers can choose to use an implements clause for ToJson. But otherwise it can be omitted.

class MySerializableClass implements ToJson {
  Map<String, dynamic> jsonEncode() => {"foo": "bar"};
}

Then warn when dynamic is used unexpectedly. Tell the developer to use an explicit type or a structural interface instead.

The difference between dynamic and Object is that the former may be structurally casted. The latter should not be. This maintains dynamic semantics in the language without it being a footgun. And it covers a majority of places where dynamic is still used today.

@maeddin
Copy link
Author

maeddin commented Jul 11, 2023

Downstream consumers can choose to use an implements clause for ToJson. But otherwise it can be omitted.

This sounds like a method to prepare for the breaking change, but not to prevent a breaking change. Here, one would continue to support something that is officially not recommended. You could throw out the support for calling toJson() without the interface with the next major Dart release. In the long-term one should not support the interface and dynamic calls for json encoding, as it offers no noticeable advantage and can be confusing for users.

@lrhn
Copy link
Member

lrhn commented Jul 11, 2023

(I'd rather just stop calling toJson entirely, interface or no interface, and require you to pass the toEncodable function. Then you can make your own ToJsonable interface and a toEncodable function which calls it.)
But that's just one example of code doing dynamic calls. We have no idea how many deliberate dynamic calls happen in client code, or even inside the VM's own libraries. I know there are some.

@ds84182
Copy link

ds84182 commented Jul 11, 2023

Flutter has various usages of dynamic, notably: https://github.com/flutter/flutter/blob/5d4a1f1f5fe1d0d0191c691eb60b6814654d6929/packages/flutter/lib/src/animation/tween.dart#L339

Which is intended to avoid having to make N different implementations of Tween to support all kinds of numeric-like classes (Offset for example, maybe Vector2/3/4 from vector_math). Could be supplanted by structural interfaces.

nickmeinhold added a commit to enspyrco/json_utils that referenced this issue Jul 25, 2023
Discussion in dart-lang/language#3192
indicates a general move away from dynamic.
@rrousselGit
Copy link

rrousselGit commented Jan 21, 2025

And doing myDynamic.fooMethod() is bad for tree-shaking. But (myDynamic as Foo).fooMethod() isn't.

@ghost
Copy link

ghost commented Jan 21, 2025

An as usage will raise eyebrows during code-review

myDynamic.fooMethod() will raise even more eyebrows.
Correct handling is not ``(myDynamic as Foo).fooMethod()`,
but

if (myDynamic is Foo) { // with promotion
   myDynamic.fooMethod();
}

There's no as here.

@rrousselGit
Copy link

myDynamic.fooMethod() will raise even more eyebrows.

No, because you often do not know that a value is dynamic because of type-inference.

Consider:

final value = await http.get(...);
print(value.entries());

value is dynamic and value.entries() is a dynamic call. But you cannot know that unless you know the signature of the http.get method.

Correct handling is not ``(myDynamic as Foo).fooMethod()`, but

if (myDynamic is Foo) { // with promotion
myDynamic.fooMethod();
}
There's no as here.

is vs as is just a matter of "what do you do when there's an unexpected state".
Throwing is very much legitimate.

It's fairly common for codebases to do:

if ( value is! Foo) {
  throw MyCustomError();
}

as is just sugar for this with AssertionError.

If you don't like exceptions, that's a different topic. You could use an error result ; and Dart could benefit from a guard statement.

You're also free to use a custom as extension:

extension on Object {
  T? cast<T>() {
    final that = this;
    if (that is T) return that;
    return null;
  }
}

Then object?.cast<Foo>()?.myMethod()

@Mike278
Copy link

Mike278 commented Jan 21, 2025

Considering Dart refers to itself as having a sound type system, it's laughable that there's built-in support for completely disabling the type checking and removing all soundness altogether.

As long as dynamic exists, Dart cannot be considered a serious contender. Meanwhile languages like Rust and Swift are thriving with their sounder-than-ever type systems.

Rust and Swift both have an Any type, which is roughly equivalent to Dart's Object? type, which is different from dynamic, neither of which violate soundness, which is a true/false where you can't be "sounder-than-ever".

@ghost
Copy link

ghost commented Jan 21, 2025

But you cannot know that unless you know the signature of the http.get method.

The linter will tell you.

If you remove dynamic and NOT introduce union types, you will have to use Object instead. But dynamic was introduced right from the start to avoid that. I remember the original discussion. dynamic means 'some union type'.

@TekExplorer
Copy link

TekExplorer commented Jan 21, 2025

But you cannot know that unless you know the signature of the http.get method.

The linter will tell you.

If you remove dynamic and NOT introduce union types, you will have to use Object instead. But dynamic was introduced right from the start to avoid that. I remember the original discussion. dynamic means 'some union type'.

Thats not what dynamic is. Dynamic is akin to "no type" since you can do dynamic access.

Object? Is the super type of everything, so it can be considered the union type of everything.

The latter is much safer in that you can't accedentally call a non-existent method

Sadly, there are parts of the language rhat do use dynamic as a union type, as the original design was inherently dynamic, and can't be represented in the current type system, which is why we need some form of union type in the language.

@lrhn
Copy link
Member

lrhn commented Jan 21, 2025

This is what a sound type system means

Well, actually … the Dart type system being sound means that the actual value of an expression will have a runtime type that is a subtype of the static type of the expression. No more or less.

What is being talked about here is (type) safety, that not having static type errors implies not having runtime type errors. Dart is not safe. It's closer than it has been, but covariant generics is still the biggest exception.
Using as or ! doesn't count, they're really user-thrown errors with a short syntax.
Using dynamic probably shouldn't count either, if it's opt in. The biggest issue is when you get an accidental or unexpected dynamic type.

Dart discourage the "implicit cast" now.

Not in any official way, no.
There are lints (or modes-that-should-be-lints) that warn about accidental dynamic types and downcasts, but they are not part of the recommended lints.
The language, style guide and recommended lints all consider dynamic perfectly fine, if "you want to disable static type checking". Just be explicit about it.

@rrousselGit
Copy link

rrousselGit commented Jan 21, 2025

Dart discourage the "implicit cast" now.

Not in any official way, no. There are lints (or modes-that-should-be-lints) that warn about accidental dynamic types and downcasts, but they are not part of the recommended lints.

This wasn't referring to dynamic in this context, but implicit-cast from strong-mode in the analysis_options file.
That used to be enabled by default, and you had to manually opt-out.

Before, on new projets, you used to be able to do:

List list = [].map();
// no toList(), Dart does an implicit cast of Iterable to List.
// This will throw at runtime instead of emitting a compilation error

That's not the case anymore. It's off by default. So in that sense, implicit-cast is officially disabled.

To me the topic of discouraging dynamic is similar.
In a loose sense, dynamic invocations are like implicit casts.

@lrhn
Copy link
Member

lrhn commented Jan 21, 2025

implicit-cast is officially disabled.

Ack. Dart removed general implicit downcasts from the language, so now the only implicit downcast is from dynamic.
Those are still fine.

@ghost
Copy link

ghost commented Jan 21, 2025

@lrhn: dynamic, together with the avoid_dynamic_calls, for all practical purposes, change the definition of dynamic from "something that has all methods" to "something of unspecified union type", and makes you cast it to one of the constituent types before use.
Please ack. 😄

@ghost
Copy link

ghost commented Jan 22, 2025

If dart can support type parameters for dynamic, we get a decent syntax for union types, e.g. dynamic<int,String>. This will force the user to handle all possibilities exhaustively (e.g. in a switch statement). This doesn't require the introduction of any new keyword or syntax (like int | String)

@mateusfccp
Copy link
Contributor

If dart can support type parameters for dynamic, we get a decent syntax for union types, e.g. dynamic<int,String>. This will force the user to handle all possibilities exhaustively (e.g. in a switch statement). This doesn't require the introduction of any new keyword or syntax (like int | String)

It would at least required something like #283 or #2532, or else we would be very limited. Alternatively, we could have dynamic<A, B> and isomorphically have multiple arguments with dynamic<int, dynamic<String, bool>>, but it would be extremely verbose.

@ghost
Copy link

ghost commented Jan 22, 2025

The feature doesn't require any generalizations. It can support dynamic<A, B, C, D> with any number of type parameters. Moreover, dynamic<A,B,C,D> would be equivalent to dynamic<B,C,A,D> and any other permutation of the types.
It will require reification though, but this is not more difficult to implement than the reification of record types (I guess).

@TekExplorer
Copy link

If you're going to do that, might as well have actual union type syntax, with A | B

Honestly I wouldn't mind if it was more of a static hint for dynamic, but it's outside the scope of this issue.

@rrousselGit
Copy link

Not sure how we diverged from "remove dynamic" to "let's make dynamic a pseudo union type".

To me this boils down to:
Lints discouraging dynamic invocation should be enabled by default.

We don't have to remove it quite yet, as that's a big breaking change.
But at minimum, we should discourage it and warn when dynamic invocations are used.

@ghost
Copy link

ghost commented Jan 22, 2025

If you discourage dynamic, you have to replace it with something. Sometimes you have to model union types somehow. How? By replacing dynamic with Object?
Here's a quote from stackoverflow Q/A (by @lrhn)

When you write Object? o = something; you are telling the system that it can't assume anything about o except that it's an Object or null. You can call toString and hashCode because those methods are defined on Object and Null, but if you try to do o.foo() you will get a warning - it can't see that you can do that, and so it warns you that your code is probably wrong.

(see https://stackoverflow.com/questions/31257735/what-is-the-difference-between-dynamic-and-object-in-dart)

If I remember the discussion of ~15 years ago correctly, this was the main justification for dynamic. This looks logical if you define dynamic as a union of some types (in any given context, these are concrete types). So far so good. But here, dart made a step that today may look illogical: instead of requiring to cast to a concrete type before use, it enabled calling whatever method, no questions asked. Why?

Remember, back then, dart was a dynamically typed language. The types were optional, so you couldn't rely on static types anyway. Dart 1 handled the types in a way that today looks quite peculiar: because VM was the only implementation, it relied on VM's runtime magic to figure out what the actual types were (in runtime!) and even optimize for them (by using on-the-fly compilation).

Why the justification for dynamic was not revisited when dart switched to static typing - I don't know (I didn't follow dart development between the versions very closely). But it doesn't matter much right now. The justification for NOT using Object in the above sense remains solid. So while deprecating dynamic, you have to replace it with ... what exactly?

@mateusfccp
Copy link
Contributor

@tatumizer

Object? works perfectly for these cases, you just have to promote it with is or (less safe, but at least explicit) as.

@rrousselGit
Copy link

Object? + is or as are more than enough.

Heck if we want to talk about unions, we have sealed classes and pattern matching now. That's union-like. It's not untagged unions, but that's definitely something.

For instance, you can do:

sealed class Json {
  factory Json.string(String str) = JsonString;
  factory Json.boolean(bool bool) = JsonBool;
  ...
}

And return that instead of Object?.

I'd sure love untagged unions. They'd be less verbose and less memory intensive in those scenarios. But we have alternatives.

@ghost
Copy link

ghost commented Jan 22, 2025

But we have alternatives.

These are not the "alternatives". They are workarounds. Why do you want a workaround for a feature that is itself a workaround?

@mateusfccp
Copy link
Contributor

These are not the "alternatives". They are workarounds.

No, they are not workarounds. At least in this specific case, they are alternatives. Something being less ergonomic or more verbose doesn't mean that it's an workaround.

@ghost
Copy link

ghost commented Jan 23, 2025

No, they are not workarounds

No, they are!
Seriously, take another look at this post.
https://stackoverflow.com/questions/31257735/what-is-the-difference-between-dynamic-and-object-in-dart

If you have an opinion opposite to the first comment in the cited thread, then just go to stackoverflow and argue against it there - it might have a greater circulation. How about that? :-)

( I fail to see how replacing dynamic with Object makes the program safer. When you have a parameter of type Object?, how do you know what types the function accepts? How does the compiler know? The idea is a total non-starter.)

@mateusfccp
Copy link
Contributor

mateusfccp commented Jan 23, 2025

I don't agree or disagree with what @lrhn is saying in the mentioned link. It's a well-stablished fact. The point in discussion here is not "what are the differences between dynamic and Object?", but "why do I need dynamic", something that is not discussed in the linked URL.

With dynamic you can "disable" the static analysis over an object. But 99% of the cases this don't add any value over what we can do to Object?, and with Object? you won't ever have the chance of unwillingly try to access an inexistent member, because you have to either promote it (with is) or cast it (with as).

The other 1% is when you want to treat an object as a structural type (in this case we are, indeed, talking about workarounds, not alternatives), something that we can kind of do with dynamic with the cost of potentially have run-time issues or regressions if ever the object in question changes its API.

Edit: Also, the main problem of this issue is not that by removing dynamic will make it impossible or hard to do things in Dart, but simply that doing so will break a lot of existing code.

@rrousselGit
Copy link

This discussion is nothing new. Typescript went through the same thing.

It started with any ; the equivalent of dynamic. But then it introduced unknown, the equivalent of Object?.
Nowadays, good TS using any is heavily discouraged, and unknown combined with is/as/pattern-matching is preferred.

To me that thread is the same thing:
Officially discourage the use of dynamic invocations, in favour of Object? + type checks/asserts.

The community pretty much is already there. Most Dart devs would swear at you if you wrote myDynamic.method() in purpose over introducing a proper interface.
It's just a matter of making that official, and promoting a few lints to the recommended list. Nothing more

@ghost
Copy link

ghost commented Jan 23, 2025

Using Object, you disable type checking anyway.
Consider

void f(Object? obj) {
  //...
}
main() {
  // who checks your types?
  f(0); 
  f([1]); 
  f({0: 0});
}

Notice that no lints will ever be able to catch the errors here - you can pass just anything, like today for dynamic.
If the compiler can help in 50% of cases, and can't help in other 50%, then it's not a typesafe solution anyway - it's a compromise, or (equivalently) ... a palliative workaround. (Even in 50% of cases where it "helps", it really doesn't': if you erroneously believe something is Foo, writing (obj as Foo).foo(); won't make your program safer: what if that obj is NOT Foo? How can the compiler know what you are allowed to cast to?)

Officially recommending this half-measure as a replacement for dynamic is not an option for a language that advertises itself as typesafe. dynamic at least explicitly says: Attention, I disable type checking here. Object doesn't have this meaning: parameter of type Object means: the function uses just the methods like hashCode, toString and others inherited from Object (refer to stackoverflow answer again). These were the arguments for the distinction between dynamic and Object from day one (discussed to death at the time). It wasn't an omission, or design mistake, or something, as you might believe. The same arguments are as valid today as they were 15 years ago. Union types are the only acceptable replacement. Please think about it.

@ghost
Copy link

ghost commented Jan 23, 2025

(Cont-d)
One may wonder: how do union types come into play at all in the discussion of dynamic?
Here's how.
Suppose your dreams come true, and dart replaces dynamic with Object?.
Consider

f(Object? obj) {
  // I know that obj is either int or String
  if (obj is int) {
   // ...
  } else if (obj is String) {
   // ...
  } else throw "...";
}

The compiler forces you to check type (with promotion) or to use the as cast to some KNOWN TYPES - in the above example, they are int or String. This begs the question: why doesn't the language allow you to express this knowledge statically?
Hence the union types.

(Interestingly, with dynamic, you were not supposed to know the types: myDynamic.foo() effectively uses "duck typing". With Object, you have to know them statically)

@mateusfccp
Copy link
Contributor

Using Object, you disable type checking anyway. Consider...

No, the code is being checked. The static type of the parameter is Object?, and the static analysis will guarantee that you only access the known members of Object?, unless you explicitly promote it.

Notice that no lints will ever be able to catch the errors here

Which errors? Your function has no definition, so we can't know which potential errors you are talking about. Either way, if the code used dynamic, the error could happen anyway, and some other errors that using Object? could prevent.

Officially recommending this half-measure as a replacement for dynamic is not an option for a language that advertises itself as typesafe.

It is at least safer than dynamic. There are other unsafety points that we could address for "a language that advertises itself as typesafe", like variance, but we are going outside of the scope of this issue.

dynamic at least explicitly says: Attention, I disable type checking here.

It doesn't always explicitly say it, because many times dynamic is inferred and can go unnoticed. In these cases, trying to call an unexistent member will cause a run-time error. If it was an Object?, this unexistent member would never be called unless explicitly (and now there's no way this wouldn't be explicit) casting with as.

Object doesn't have this meaning: parameter of type Object means: the function uses just the methods like hashCode, toString and others inherited from Object [...].

It doesn't mean the function "uses just", but that the object has at least those members, having potentially more, something you will have to explicitly check or cast.

This begs the question: why doesn't the language allow you to express this knowledge statically? Hence the union types.

I agree that untagged union types is something nice. dynamic does not solve this problem, though, and removing it won't improve or degrade the experience on this matter in any way.

Interestingly, with dynamic, you were not supposed to know the types: myDynamic.foo() effectively uses "duck typing".

Yes, it is effectively duck typing, and this is what this issue is about. We don't want the unsafety inherent to duck typing. The alternative is to have structural typing, which is not the subject of this issue.

With Object, you have to know them statically.

Which is the point of this issue.

@TekExplorer
Copy link

I actually wouldn't say that duck typing is inherently unsafe.

Dynamic access is unsafe - it's just that we can use it to emulate duck typing.

Hell, I think I saw a package support rxdart's Value stream without depending on the package by using this.

Proper duck typing can be statically typed, as I believe golang does.

Honestly, Im tempted to want duck typing every time I use jsonEncode :/

That said, it does seem clear that dynamic should still exist, but it is my opinion that it should not be allowed for return types or closure arguments.

Any usage of it should be extremely deliberate

@mateusfccp
Copy link
Contributor

Proper duck typing can be statically typed, as I believe golang does.

According to Wikipedia,

Structural typing is a static typing system that determines type compatibility and equivalence by a type's structure, whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during runtime.

If I understand correctly, duck typing is inherently dynamic, and, thus, unsafe. The static version of duck typing is structural typing.

@TekExplorer
Copy link

I see. Makes sense. So I had the right idea but the wrong terminology.

@karroze
Copy link

karroze commented Feb 21, 2025

Absolutely agreed, dynamic is obsolete for many years now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests