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:
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
:
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 double
annotation:
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
.
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¶
- Create a constant called
age1
and set it equal to42
. Create another constant calledage2
and set it equal to21
. Check that the type for both constants has been inferred correctly asint
by hovering your mouse pointer over the variable names in VS Code. - Create a constant called
averageAge
and set it equal to the average ofage1
andage2
using the operation(age1 + age2) / 2
. Hover your mouse pointer overaverageAge
to check the type. Then check the result ofaverageAge
. Why is it adouble
if the components are allint
?
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 adouble
. - 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.