跳转至

3 Types & Operations

Life is full of variety, and that variety expresses itself in different ways. What type of toothpaste do you use? Spearmint? Cinnamon? What’s your blood type? A? B? O+? What type of ice cream do you like? Vanilla? Strawberry? Praline pecan fudge swirl? Having names for all of these different things helps you talk intelligently about them. It also helps you to recognize when something is out of place. After all, no one brushes their teeth with praline pecan fudge swirl. Though it does sound kind of nice.

Programming types are just as useful as real-life types. They help you to categorize all the different kinds of data you use in your code.

In Chapter 2, “Expressions, Variables & Constants”, you learned how to name data using variables and also got a brief introduction to Dart data types. In this chapter, you’ll learn even more about types and what you can do with them.

Data Types in Dart

In Dart, a type is a way to tell the compiler how you plan to use some data. By this point in this book, you’ve already seen the following types:

  • int
  • double
  • num
  • dynamic
  • String

The last one in that list, String, is the type used for text like 'Hello, Dart!'.

Just as you don’t brush your teeth with ice cream, Dart types keep you from trying to do silly things like dividing text or removing whitespace from a number.

Dart has even more built-in types than just the ones listed above. The basic ones, such as int, double, and num will serve you adequately in a great variety of programming scenarios, but when working on projects with specific needs, it’ll be convenient to create custom types instead. A weather app, for example, may need a Weather type, while a social media app may need a User type. You’ll learn how to create your own types in Chapter 5, “Control Flow”, and Chapter 8, “Classes”.

As you learned in Chapter 2, “Expressions, Variables & Constants”, types like int, double and num are subclasses, or subtypes, of the Object type. Object defines a few core operations, such as testing for equality and describing itself. Every non-nullable type in Dart is a subtype of Object, and as a subtype, shares Object’s basic functionality.

Note

You’ll learn about nullable types in Chapter 11, “Nullability”.

Type Inference

In the previous chapter, you also got a sneak peek at type inference, which you’ll look at in more depth now.

Annotating Variables Explicitly

It’s fine to always explicitly add the type annotation when you declare a variable. This means writing the data type before the variable name.

int myInteger = 10;
double myDouble = 3.14;

You annotated the first variable with int and the second with double.

Creating Constant Variables

Declaring variables the way you did above makes them mutable. If you want to make them immutable, but still keep the type annotation, you can add const or final in front.

These forms of declaration are fine with const:

const int myInteger = 10;
const double myDouble = 3.14;

They’re also fine with final:

final int myInteger = 10;
final double myDouble = 3.14;

Note

Mutable data is convenient to work with because you can change it any time you like. However, many experienced software engineers have come to appreciate the benefits of immutable data. When a value is immutable, that means you can trust that no one will change that value after you create it. Limiting your data in this way prevents many hard-to-find bugs from creeping in, and also makes the program easier to reason about and to test.

Letting the Compiler Infer the Type

While it’s permissible to include the type annotation as in the example above, it’s redundant. You’re smart enough to know that 10 is an int and 3.14 is a double, and it turns out the Dart compiler can deduce this as well. The compiler doesn’t need you to explicitly tell it the type every time — it can figure the type out on its own through a process called type inference. Not all programming languages have type inference, but Dart does — and it’s a key component behind Dart’s power as a language.

You can simply drop the type in most instances. For example, here are the constants from above without the type annotations:

const myInteger = 10;
const myDouble = 3.14;

Dart infers that myInteger is an int and myDouble is a double.

Checking the Inferred Type in VS Code

Sometimes, it can be useful to check the inferred type of a variable or constant. You can do this in VS Code by hovering your mouse pointer over the variable name. VS Code will display a popover like this:

img

VS Code shows you the inferred type. In this example, the type is int.

It works for other types, too. Hovering your mouse pointer over myDouble shows that it’s a double:

img

Type inference isn’t magic; Dart is simply doing what your own brain does very easily. Programming languages that don’t use type inference often feel verbose, because you need to specify the (usually) obvious type each time you declare a variable or constant.

Note

There are times when you’ll want (or need) to explicitly include the type, either because Dart doesn’t have enough information to figure it out, or because you want your intent to be clear to the reader. However, you’ll see type inference used for most of the code examples in this book.

Checking the Type at Runtime

Your code can’t hover a mouse pointer over a variable to check the type, but Dart does have a programmatic way of doing nearly the same thing: the is keyword.

num myNumber = 3.14;
print(myNumber is double);
print(myNumber is int);

Run this to see the following result:

true
false

Recall that both double and int are subtypes of num. That means myNumber could store either type. In this case, 3.14 is a double, and not an int, which is what the is keyword checks for and confirms by returning true and false respectively. You’ll learn more about the type for true and false values in Chapter 5, “Control Flow”.

Another option to see the type at runtime is to use the runtimeType property that is available to all types.

print(myNumber.runtimeType);

This prints double as expected.

Type Conversion

Sometimes, you’ll have data in one type, but need to convert it to another. The naïve way to attempt this would be like so:

var integer = 100;
var decimal = 12.5;
integer = decimal;

Dart will complain if you try to do that:

A value of type 'double' can't be assigned to a variable of type 'int'.

Some programming languages aren’t as strict and will perform conversions like this silently. Experience shows this kind of silent, implicit conversion is a frequent source of software bugs and often hurts code performance. Dart disallows you from assigning a value of one type to another and avoids these issues.

Remember, computers rely on programmers to tell them what to do. In Dart, that includes being explicit about type conversions. If you want the conversion to happen, you have to say so!

Instead of simply assigning and hoping for implicit conversion, you need to explicitly say that you want Dart to convert the type. You can convert this double to an int like so:

integer = decimal.toInt();

The assignment now tells Dart, unequivocally, that you want to convert from the original type, double, to the new type, int.

Note

In this case, assigning the decimal value to the integer results in a loss of precision: The integer variable ends up with the value 12 instead of 12.5. This is why it’s important to be explicit. Dart wants to make sure you know what you’re doing and that you may end up losing data by performing the type conversion.

Operators With Mixed Types

So far, you’ve only seen operators acting independently on integers or doubles. But what if you have an integer that you want to multiply with a double?

Take this example:

const hourlyRate = 19.5;
const hoursWorked = 10;
const totalCost = hourlyRate * hoursWorked;

hourlyRate is a double and hoursWorked is an int. What will the type of totalCost be? It turns out that Dart will make totalCost a double. This is the safest choice, since making it an int could cause a loss of precision.

If you actually do want an int as the result, then you need to perform the conversion explicitly:

const totalCost = (hourlyRate * hoursWorked).toInt();

The parentheses tell Dart to do the multiplication first, and after that, to take the result and convert it to an integer value. However, the compiler complains about this:

Const variables must be initialized with a constant value.

The problem is that toInt is a runtime method. This means that totalCost can’t be determined at compile time, so making it const isn’t valid. No problem; there’s an easy fix. Just change const to final:

final totalCost = (hourlyRate * hoursWorked).toInt();

Now totalCost is an int.

Ensuring a Certain Type

Sometimes you want to define a constant or variable and ensure it remains a certain type, even though what you’re assigning to it is of a different type. You saw earlier how you can convert from one type to another. For example, consider the following:

const wantADouble = 3;

Here, Dart infers the type of wantADouble as int. But what if you wanted the constant to store a double instead?

One thing you could do is the following:

final actuallyDouble = 3.toDouble();

This uses type conversion to convert 3 into a double before assignment, as you saw earlier in this chapter.

Another option would be to not use type inference at all, and to add the doubleannotation:

const double actuallyDouble = 3;

The number 3 is an integer, but literal number values that contain a decimal point cannot be integers, which means you could have avoided this entire discussion had you started with:

const wantADouble = 3.0;

Sorry! :]

Casting Down

The image below shows a tree of the types you’ve encountered so far. Object is a supertype of num and String, and num is a supertype of int and double. Conversely, int and double are subtypes of num, which is a subtype of Object.

img

Types at the top of the tree can only perform very general tasks. The further you go down the tree, the more specialized the types become and the more detailed their tasks.

At times, you may have a variable of some general supertype, but you need functionality that is only available in a subtype. If you’re sure that the value of the variable actually is the subtype you need, then you can use the as keyword to change the type. This is known as type casting. When type casting from a supertype to a subtype, it’s called downcasting.

Here’s an example:

num someNumber = 3;

You have a number, and you want to check if it’s even. You know that integers have an isEven property, so you attempt the following:

print(someNumber.isEven);

However, the compiler gives you an error:

The getter 'isEven' isn't defined for the type 'num'.

num is too general of a type to know anything about even or odd numbers. Only integers can be even or odd. The issue is that num could potentially be a double at runtime since num includes both double and int. In this case, though, you’re sure that 3 is an integer, so you can cast someNumber to int.

final someInt = someNumber as int;
print(someInt.isEven);

The as keyword causes the compiler to recognize someInt as an int, so your code is now able to use the isEven property that belongs to the int type. Since 3 isn’t even, Dart prints false.

You need to be careful with type casting, though. If you cast to the wrong type, you’ll get a runtime error:

num someNumber = 3;
final someDouble = someNumber as double;

This will crash with the following message:

_CastError (type 'int' is not a subtype of type 'double' in type cast)

The runtime type of someNumber is int, not double. In Dart, you’re not allowed to cast to a sibling type, such as int to double. You can only cast down to a subtype.

If you do need to convert an int to a double at runtime, use the toDouble method that you saw earlier:

final someDouble = someNumber.toDouble();

Exercises

  1. Create a constant called age1 and set it equal to 42. Create another constant called age2 and set it equal to 21. Check that the type for both constants has been inferred correctly as int by hovering your mouse pointer over the variable names in VS Code.
  2. Create a constant called averageAge and set it equal to the average of age1and age2 using the operation (age1 + age2) / 2. Hover your mouse pointer over averageAge to check the type. Then check the result of averageAge. Why is it a double if the components are all int?

Object and dynamic Types

Dart grew out of the desire to solve some problems inherent in JavaScript. JavaScript is a dynamically-typed language. Dynamic means that something can change, and for JavaScript that means the types can change at runtime.

Here is an example in JavaScript:

var myVariable = 42;
myVariable = "hello";

In JavaScript, the first line is a number and the second line a string. Changing the types on the fly like this is completely valid in JavaScript. While this may be convenient at times, it makes it really easy to write buggy code. For example, you may be erroneously thinking that myVariable is still a number, so you write the following code:

var answer = myVariable * 3; // runtime error

Oops! That’s an error because myVariable is actually a string, and the computer doesn’t know what to do with “hello” times 3. Not only is it an error, you won’t even discover the error until you run the code.

You can totally prevent mistakes like that in Dart because it’s an optionally-typedlanguage. That means you can choose to use Dart as a dynamically typed language or as a statically-typed language. Static means that something cannot change; once you tell Dart what type a variable is, you’re not allowed to change it anymore.

If you try to do the following in Dart:

var myVariable = 42;
myVariable = 'hello'; // compile-time error

The Dart compiler will immediately tell you that it’s an error. That makes type errors trivial to detect.

As you saw in Chapter 2, “Expressions, Variables & Constants”, the creators of Dart did include a dynamic type for those who wish to write their programs in a dynamically-typed way.

dynamic myVariable = 42;
myVariable = 'hello'; // OK

In fact, this is the default if you use var and don’t initialize your variable:

var myVariable; // defaults to dynamic
myVariable = 42;      // OK
myVariable = 'hello'; // OK

While dynamic is built into the system, it’s more of a concession rather than an encouragement to use it. You should still embrace static typing in your code as it will prevent you from making silly mistakes.

If you need to explicitly say that any type is allowed, you should consider using the Object? type.

Object? myVariable = 42;
myVariable = 'hello'; // OK

At runtime, Object? and dynamic behave nearly the same. However, when you explicitly declare a variable as Object?, you’re telling everyone that you generalized your variable on purpose and that they’ll need to check its type at runtime if they want to do anything specific with it. Using dynamic, on the other hand, is more like saying you don’t know what the type is; you’re telling people they can do what they like with this variable, but it’s completely on them if their code crashes.

Note

You may be wondering what that question mark at the end of Object?is. That means that the type can include the null value. You’ll learn more about nullability in Chapter 11, “Nullability”.

Challenges

Before moving on, here are some challenges to test your knowledge of types and operations. It’s best if you try to solve them yourself, but solutions are available with the supplementary materials for this book if you get stuck.

Challenge 1: Teacher’s Grading

You’re a teacher, and in your class, attendance is worth 20% of the grade, the homework is worth 30% and the exam is worth 50%. Your student got 90 points for her attendance, 80 points for her homework and 94 points on her exam. Calculate her grade as an integer percentage rounded down.

Challenge 2: What Type?

What’s the type of value?

const value = 10 / 2;

Key Points

  • Type conversion allows you to convert values of one type into another.
  • When doing operations with basic arithmetic operators (+, -, *, /) and mixed types, the result will be a double.
  • Type inference allows you to omit the type when Dart can figure it out.
  • Dart is an optionally-typed language. While it’s preferable to choose statically-typed variables, you may write Dart code in a dynamically-typed way by explicitly adding the dynamic type annotation in front of variables.