Irbisa · Cheatsheet

Dart Type System in Depth

Top and bottom types, reified generics, variance, inference, FutureOr, extension types, Never

Senior Developer · 18 questions

  1. 01

    Dart has three top types: Object?, dynamic and void. How do they differ, and why is Object not one of them?

    All three hold every value, null included; they differ only in what the compiler lets you do with it.

  2. 02

    What is the Never type, and when should a function return Never instead of void?

    Never is the bottom type: a subtype of every type with no values at all, so an expression of type Never never completes; it throws or loops forever.

    // wrong: void; the call is an ordinary statement, flow continues past itvoid failVoid(String m) => throw StateError(m); // right: Never; the call does not returnNever fail(String m) => throw StateError(m);…
  3. 03

    What does it mean that Dart's type system is sound, and which type checks still happen at run time?

    Sound means a static type never lies: an expression of type String always evaluates to a String, or the program throws before the wrong value is used.

  4. 04

    How does flow analysis decide what a null check or is test promotes, and what cancels the promotion?

    Flow analysis narrows the static type of a local variable or parameter after a check such as x is String or x != null, so you can use it as the narrower type without a cast.

    void demo(Object value, String? path) {  if (value is! String) return;  print(value.length); // String for the rest of the body   if (path == null) throw ArgumentError.notNull('path');  print(path.length); // String: the null branch cannot continue…
  5. 05

    Dart generics are reified, unlike Java's. What can you do with a type argument at run time, and what still can't you do?

    Every generic object carries its type arguments at run time, so is List<String> and as Map<String, int> are real checks and a type parameter T is a real type inside the body.

    import 'dart:convert'; void main() {  final tags = jsonDecode('["a", "b"]');  print(tags.runtimeType); // List<dynamic>, fixed when the list was created…
  6. 06

    What is the difference between x is T and x.runtimeType == T, and why should runtimeType stay out of your logic?

    is asks a subtype question and promotes the variable; runtimeType == T compares the exact class, type arguments included.

    import 'dart:convert'; class Animal {} class Dog extends Animal {  void bark() {}…
  7. 07

    How does Dart's type inference work, and when does it silently fall back to dynamic?

    Inference fills in the types you did not write, in two directions: downwards from the context type (List<String> a = [];) and upwards from the expressions themselves ([1, 2] is a List<int>).

    import 'dart:convert'; class Animal {}class Dog extends Animal {}class Cat extends Animal {}…
  8. 08

    This code assigns a List<Dog> to a List<Animal> and then adds a Cat. Does it compile, and what happens when it runs?

    class Animal {}class Dog extends Animal {}class Cat extends Animal {} void addStray(List<Animal> animals) => animals.add(Cat()); void main() {  final dogs = <Dog>[Dog()];  final List<Animal> animals = dogs;  print(animals.length);  addStray(animals);  print(dogs.length);}

    It compiles, prints 1, and then addStray throws type 'Cat' is not a subtype of type 'Dog' of 'value'.

  9. 09

    What does the covariant keyword do on a parameter, and why should operator == keep Object other?

    covariant lets an override narrow a parameter to a subtype, which the normal override rule forbids.

    class Animal {} class Dog extends Animal {} abstract class Handler {  void handle(covariant Animal a); // overrides may narrow `a`…
  10. 10

    This parser has no as anywhere, yet it crashes with type 'Null' is not a subtype of type 'int'. Where did the cast come from?

    class User {  final int id;  final String name;  User(this.id, this.name);   factory User.fromJson(Map<String, dynamic> json) {    final int id = json['id'];    return User(id, json['name']);  }} void main() {  print(User.fromJson({'name': 'Ada'}).name);}

    json['id'] has the static type dynamic, and assigning a dynamic to an int is an implicit downcast: the compiler inserts the same checked cast that as int would.

  11. 11

    Find the bug: get<AuthRepository>() throws "AuthRepository is not registered" right after the app registered the repository.

    abstract interface class AuthRepository {}class HttpAuthRepository implements AuthRepository {} class Locator {  final _services = <Type, Object>{};   void register<T extends Object>(T service) => _services[T] = service;   T get<T extends Object>() =>      _services[T] as T? ?? (throw StateError('$T is not registered'));} void main() {  final locator = Locator()..register(HttpAuthRepository());  print(locator.get<AuthRepository>());}

    register(HttpAuthRepository()) infers T from the argument, so the service is stored under the key HttpAuthRepository.

  12. 12

    Why can a void Function(Object) be passed where a void Function(Dog) is expected, while a void Function(Dog) cannot fill a void Function(Animal) slot?

    Function types are contravariant in their parameters and covariant in their return type: a substitute may accept more than the slot promises and return something narrower.

    class Animal {} class Dog extends Animal {  void fetch() {}}…
  13. 13

    Your maxOf<T extends Comparable<T>> works for DateTime and your own Money, but maxOf<int> will not compile. Why, and what do you write instead?

    int is only a Comparable<num>, and Comparable<int> is a subtype of Comparable<num>, never the reverse, so T extends Comparable<T> rejects int.

    // F-bound: T must be comparable to its own kindT maxOf<T extends Comparable<T>>(List<T> xs) =>    xs.reduce((a, b) => a.compareTo(b) >= 0 ? a : b); class Money implements Comparable<Money> {  final int cents;…
  14. 14

    What is an extension type, how does it differ from a wrapper class, and where does its guarantee disappear at run time?

    An extension type (Dart 3.3) is a compile-time view of an existing type: extension type UserId(String value) gives a String its own static type and API with no wrapper object.

    extension type UserId(String value) {  bool get isValid => value.isNotEmpty;  // String toString() => ...; // error: conflicts with a member of Object} // `implements` exposes the representation's members…
  15. 15

    Find the bug: full phone numbers show up in the logs, although every log field goes through mask.

    extension type Email(String value) {}extension type Phone(String value) {} // Masks personal data before a log field is written.String mask(Object? field) {  if (field is Email) {    return '***@${field.value.split('@').last}';  }  if (field is Phone) {    return '***${field.value.substring(field.value.length - 4)}';  }  return '***';} void main() => print(mask(Phone('+77011234567')));

    Extension types are erased at run time, so field is Email only checks is String and is true for every String.

  16. 16

    What does this print, and what does it reveal about FutureOr<T> as a type?

    import 'dart:async'; FutureOr<Object> load() => Future<String>.value('data'); Future<void> main() async {  final raw = load();  print(raw is Future);  final v = await raw;  print(v.runtimeType);  print(<FutureOr<Object?>>[] is List<Object?>);}

    It prints true, String, true.

  17. 17

    A codebase full of Map<String, dynamic> analyses clean and still crashes on casts. What do strict-casts, strict-raw-types and strict-inference each catch?

    strict-casts catches those crashes: it forbids implicit downcasts from dynamic, so String name = json['name']; becomes an analyzer error.

    # analysis_options.yamlinclude: package:flutter_lints/flutter.yaml analyzer:  language:    strict-casts: true      # String name = json['name'];  -> error…
  18. 18

    You are publishing a package whose main class is generic. How do you keep its public API type-safe for callers?

    Bound type parameters to Object so null cannot mean two things, keep dynamic and raw types out of every public signature, accept read-only Iterable<T>, and return copies, not your internal collections.

    // wrong: unbounded; Cache<String, int?> makes `null` mean two thingsclass LooseCache<K, V> {  final _m = <K, V>{};  V? get(K key) => _m[key]; // "absent" or "stored null"?}…