skip to content

In Dart, why is typing a callback parameter as plain `Function` weaker than a precise type such as `bool Function(String)`?

level: middleimportance: should knowfreq 44%

answer

  1. supertype of every function type
  2. no parameters, no return type
  3. calls become dynamic invocations
  4. mismatches surface only at run time
  5. exception: several accepted arities

basics

~20 s

Plain Function is the supertype of every function type and carries no signature, so calls through it are dynamic: arguments go unchecked until run time and the result is dynamic. A precise type like bool Function(String) is checked at compile time.

solid answer

~40 s

`Function` is the supertype of every function type, but it says nothing about parameters or return type. Calling a value typed `Function` is a **dynamic invocation**, as unsafe as calling through `dynamic`: the analyzer accepts any argument list, a mismatch fails with an error at run time, and the result is `dynamic`, so the lack of checking spreads. A lambda passed into a `Function` parameter also gets no context type, so its parameters are not inferred. A precise type such as `bool Function(String)` lets the analyzer reject a wrong callback at the call site, infer lambda parameters and type the result. Effective Dart says to prefer full signatures; the accepted exception is an API taking several arities, like `Future.catchError`, whose `onError` may take the error alone or the error and a stack trace.

go deeper

for a junior

Know that Function alone accepts any function but tells the compiler nothing about its parameters or its return type.

for a middle

Explain that calling a Function-typed value is a dynamic invocation, checked only at run time, with a dynamic result and no inference for lambdas passed in.

for a senior

Tighten such signatures in review, enable avoid_dynamic_calls where the guarantee pays, and recognise the multi-arity exception that catchError and Stream.listen rely on.

for a principal

Frame it as API design: a precise callback type is documentation the compiler enforces, and loosening it to Function trades that for flexibility few APIs need.

## What the `Function` type is In `dart:core`, `Function` is declared as an `abstract final class`. It is the **supertype of all function types**: every closure, tear-off and top-level function is assignable to it, and every object whose runtime type is a function type implements it. What it does *not* carry is any information about the **parameter list** or the **return type**. Since Dart 3.0 the class is `final`, so `implements Function`, `extends Function` or `with Function` are compile-time errors in code at language version 3.0 or later. Before that the clauses were allowed but had no effect. ## What a call through `Function` costs A value whose static type is `Function` can still be called, but the SDK documents that such a call is a **dynamic invocation**, exactly as if the value had been typed `dynamic`. Concretely: 1. **No static argument check.** `f('not', 'one', 'int')` compiles even when `f` takes a single `int`. 2. **Failure at run time.** The argument list is compared with the function's real parameters only when the call runs, and a mismatch fails with an `Error`. 3. **A `dynamic` result.** The return type is unknown, so the result is `dynamic` and every member access on it is unchecked too. 4. **No inference for lambdas.** A function literal passed where `Function` is expected has no function-typed context, so its parameters are not inferred from the call site. 5. **Runtime cost.** The `avoid_dynamic_calls` lint notes that dynamic calls carry compile-size and runtime-performance penalties on most production compilers. ## Side by side | Aspect | `Function test` | `bool Function(String) test` | |---|---|---| | Which functions are accepted | Any function at all | Only one-`String`-in, `bool`-out shapes | | Argument check on `test(x)` | At run time | At compile time | | Static type of `test(x)` | `dynamic` | `bool` | | Inference of `(v) => ...` passed in | None | `v` is `String` | | Analyzer help on refactor | None | Every mismatched caller is reported | ```dart bool isValidLoose(String value, Function test) => test(value); // dynamic call bool isValid(String value, bool Function(String) test) => test(value); void main() { isValid('abc', (v) => v.isEmpty); // v inferred as String isValidLoose('abc', (int n) => n > 0); // compiles, then fails at run time } ``` ## The legitimate exception Dart has no union types, so one function type cannot say *either one parameter or two*. Where an API genuinely accepts several arities, `Function` is the least-bad choice, and Effective Dart names this exception explicitly: - `Future.catchError(Function onError, ...)` accepts `(error)` or `(error, stackTrace)`. - `Future.then` has an optional `Function? onError` for the same reason. - `Stream.listen`'s `onError` must be `void Function(Object error)` or `void Function(Object error, StackTrace)`, and the implementation checks which one it received. That check is worth copying when you must accept several arities yourself: the SDK's stream code tests `onError is void Function(Object, StackTrace)` first and `onError is void Function(Object)` second, and only then calls it. After a successful `is` test the value is promoted to the precise function type, so the call itself is statically checked. The dynamic part is confined to one branch point, and an unexpected shape can be rejected with a clear `ArgumentError` instead of an obscure failure deep inside a later call. Outside that case, a bare `Function` in an API is usually an unfinished signature. ## `Function.apply` The `Function` class also offers `Function.apply(function, positionalArguments, namedArguments)`, which calls a function with a `List` of positional arguments and a `Map<Symbol, dynamic>` of named ones. It behaves like any other dynamic call: mismatches are errors at run time. It suits genuinely data-driven dispatch, such as a small command table or a test harness, and nothing else. ## Catching it in analysis - The **`avoid_dynamic_calls`** lint flags calls through `dynamic` and, per its documentation, calls through values typed `Function`, whose semantics it calls close to identical to `dynamic`. - It is **not** in the core, recommended or flutter lint sets, so a team enables it in `analysis_options.yaml` when it wants the guarantee. - Casting with `as Function` or `as dynamic` is the documented way to mark an intentional dynamic call. The interview point is simple: a precise function type is documentation the compiler enforces, and `Function` throws that away.

  • What does the `avoid_dynamic_calls` lint say about values typed `Function`?
    It treats them like `dynamic`: its documentation says `Function`'s semantics are close to identical to `dynamic`, so calling a `Function`-typed value triggers the lint. The rule is not in the core, recommended or flutter sets, so you enable it yourself in `analysis_options.yaml`. A cast with `as Function` marks a deliberate dynamic call and is allowed.
  • When is `Function.apply` appropriate in Dart?
    Only when the argument list is really assembled at run time, such as a command dispatcher or test harness. `Function.apply(fn, positional, named)` takes a `List` of positional arguments and a `Map<Symbol, dynamic>` of named ones and checks them only when the call runs, failing the way any dynamic call does. Ordinary code should use a precise function type instead.

A plain Function parameter is a socket labelled only 'plug goes here': anything with prongs fits, and you find out it was the wrong voltage when it sparks. A precise function type is a keyed socket that refuses the wrong plug before power flows.

saying these in an interview costs you the question

  • Function and void Function() are interchangeable annotations
  • The compiler still checks arguments when you call a Function-typed value
  • A Function-typed callback gives the caller its declared return type
  • A class can implement Function to become callable
  • Plain Function is never acceptable in a public API