---
title: 'Basic Types'
source: 'https://academia.sh/en/courses/swift/basic-types'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:11+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tell apart the roles of the `Int`, `Double`, `Bool` and `String` types'
    - 'State that conversion between types is written explicitly and when it is performed'
    - 'Explain why a floating-point comparison gives an unexpected result'
    - 'Show that tuple elements are evaluated from left to right, even when they are not read'
---

# Basic Types

Introduces the `Int`, `Double`, `Bool` and `String` types; measures that conversion is explicit and that a conversion expression is evaluated immediately.

We saw the moment of assignment for a constant and a variable: on the line where it is defined, **immediately**. Now we look at the assigned value itself. Swift has four basic types: `Int` for integers, `Double` for floating-point numbers, `Bool` for logical values and `String` for text. This lesson asks one question: when is an expression that converts one type to another evaluated?

## Conversion Is Explicit

Swift does not mix two types on its own. If you try to add an `Int` and a `Double`, the compiler rejects the program: the snippet below does not compile in Swift 6.4, which is why it has no output.

```swift
let count = 1
let size = 2.5
print(count + size)
```

The fix is to write the conversion: `Double(count)`. A conversion is an expression of its own, and if it is written inside a `let`, it is evaluated once, on that line. This lesson's measurement confirms it. First the helper file that stays the same in every lesson.

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```

The measurement starts with two `Probe` objects: one tracks the conversion, and the other tracks a tuple element. Next to the conversion there are two floating-point observations and a print of `Int.max`, because why a `Double` comparison surprises is this lesson's subject. The helper function `marked` shows the order of the tuple elements with `print`. `unusedProbe` tracks a tuple element that is never read.

```swift
// main.swift — conversion and tuple moments
let conversionProbe = Probe("type-conversion")
let tupleProbe = Probe("tuple-element")
let unusedProbe = Probe("unused-element")

let wholeCount = 7
let ratio = conversionProbe.eval(Double(wholeCount) / 2)
_ = conversionProbe.use { ratio }
_ = conversionProbe.use { ratio }
print(ratio)
print(Int.max)
print(0.1 + 0.2 == 0.3)
print(0.1 + 0.2)

func marked(_ n: Int) -> Int { print("value \(n)"); return n }
let triple = (marked(1), marked(2), marked(3))
print(triple.1)

let record = (size: tupleProbe.eval(3), spare: unusedProbe.eval(9))
_ = tupleProbe.use { record.size }
_ = tupleProbe.use { record.size }

let allProbes = [conversionProbe, tupleProbe, unusedProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
```

```
3.5
9223372036854775807
false
0.30000000000000004
value 1
value 2
value 3
2
type-conversion: 1 evaluations, 2 uses -> immediately
tuple-element: 1 evaluations, 2 uses -> immediately
unused-element: 1 evaluations, 0 uses -> immediately
immediately: 3
on-first-use: 0
on-every-use: 0
never: 0
```

`ratio` gives seven divided by two as 3.5, because the division was done on `Double`. `Int.max` prints the largest `Int` value; one more than this value is a program error and was left out of the lesson's measurement. `0.1 + 0.2 == 0.3` gives `false`, against expectation: the addition does not produce exactly 0.3 but a number very close to it. Because a floating-point number cannot be represented exactly in base two, you measure a difference instead of testing equality; this is not a Swift feature but a consequence of the `Double` representation.

The conversion line was evaluated once, on the line where it is defined, before the first use: `type-conversion` came out `immediately` with two uses. The tuple elements were evaluated from left to right: `value 1`, `value 2`, `value 3`. A tuple is a structure that gathers several values into a single value; its elements are ready on the definition line.

**LB16.** The `Int.max` print is valid only for a 64-bit `Int`; the compiler used for the measurement compiled for a 64-bit target.

**LB17.** `0.1 + 0.2 == 0.3` was measured with two constant expressions; whether the evaluation that does the arithmetic happens in the compiler or at run time is not told apart in this lesson.

## Numeric Type Initializers

Swift does not convert implicitly between different numeric types; you write the conversion. The tool for it is the type's initializer: `Double(7)`, `Int(3.9)`, `UInt8(200)`. The initializer that turns a fractional value into an `Int` drops the fractional part, that is, it truncates toward zero; `Int(3.9)` gives 3, and `Int(-3.9)` gives -3. If you need rounding, you write a separate operation.

To measure this behavior, the block below produces a fractional number through a `Probe` and passes it through two different initializers. The initializer is the expression itself; so the `eval` call that produces the number sits inside the argument of the initializer.

```swift
// Convert.swift — conversion with initializers
let truncateProbe = Probe("truncating-conversion")
let widenProbe = Probe("widening-conversion")
let cut = Int(truncateProbe.eval(3.9))
let negativeCut = Int(truncateProbe.eval(-3.9))
let wide = Double(widenProbe.eval(7))
let small = UInt8(widenProbe.eval(200))
_ = truncateProbe.use { cut + negativeCut }
_ = truncateProbe.use { cut - negativeCut }
_ = widenProbe.use { wide }
_ = widenProbe.use { small }
print(cut, negativeCut, wide, small)
truncateProbe.report()
widenProbe.report()
```

```
3 -3 7.0 200
truncating-conversion: 2 evaluations, 2 uses -> on-every-use
widening-conversion: 2 evaluations, 2 uses -> on-every-use
```

`Int(3.9)` gave 3 and `Int(-3.9)` gave -3; that is, the conversion truncates toward zero. `Double(7)` was printed as `7.0`, and `UInt8(200)` stayed 200. Both measurement lines showed two evaluations: each initializer call evaluated its argument once, and because each `Probe` is tied to two initializers, the count rose to two. That is why both lines read `on-every-use`; the count is the product of the measurement harness, not of the conversion.

What matters here is the place where an initializer can fail: a value like `UInt8(300)` does not fit this type, and the program stops. So when you write a conversion, you need to know in advance that the value fits the target type. The lesson does not run this case.

**LB18.** The conversions were measured only with values that fit the type; the case of a value that does not fit is not run in this lesson, only described.

## Overflow Operators

Integer types have limits. A `UInt8` value stays between 0 and 255. If the regular `+` operator pushes the result past this limit, the program stops. Swift also offers operators that accept overflow: `&+`, `&-` and `&*`. These operators wrap the result around according to the type's capacity. The `&` prefix is the way to say that you want overflow on purpose; it does not happen silently, it happens by choosing the operator.

The block below adds a number to a `UInt8` value with `&+` inside a function. Because the added number comes through a `Probe`, each call counts as one evaluation. The first call adds 10 to 250 and passes the limit; the second call works with a safe value.

```swift
// Wrap.swift — the overflow operator wraps around
func bump(_ value: UInt8, probe: Probe) -> UInt8 {
    value &+ probe.eval(10)
}
let wrapProbe = Probe("overflow-operator")
print(wrapProbe.use { bump(250, probe: wrapProbe) })
print(wrapProbe.use { bump(1, probe: wrapProbe) })
let floor: UInt8 = 0
print(floor &- 1, UInt8.max)
wrapProbe.report()
```

```
4
11
255 255
overflow-operator: 2 evaluations, 2 uses -> on-every-use
```

`250 &+ 10` did not come out 260; the result was 4, because after 255 the count returned to 0. `1 &+ 10` gave the normal 11. On the third line, `0 &- 1` came out 255, the same as `UInt8.max`: the number that went below the limit wrapped around to the upper end. The measurement line shows two evaluations: each `bump` call evaluated its operand once, so the moment is `on-every-use`.

Overflow operators can be measured because the program does not stop. The same overflow with the regular `+` does stop it, and for that reason it is not run in this lesson. Which one you choose depends on your intent: write `&+` if you want wrapping, and `+` if you want overflow to be an error.

**LB19.** The overflow operator was measured with `UInt8`; the wrapping behavior of signed types with `&+` was not measured in this lesson.

## Type Aliases and Limits

Every numeric type has a lower and an upper limit, and the type exposes them as the static properties `min` and `max`: `Int8.min` is -128, `Int8.max` is 127, `UInt8.max` is 255. Knowing a type's limit answers the overflow question on the spot. When you want to give a type another name in code, you use `typealias`: `typealias Pixel = UInt8`. An alias is not a new type; it is a second name for the same type and carries its limits and behavior unchanged.

The block below defines the `Pixel` alias, binds a constant with it, and prints the constant's type and limits. The constant comes from a `Probe` and is read twice.

```swift
// Alias.swift — a type alias is the same type
typealias Pixel = UInt8
let aliasProbe = Probe("alias-constant")
let brightness: Pixel = aliasProbe.eval(200)
_ = aliasProbe.use { brightness }
_ = aliasProbe.use { brightness &+ 100 }
print(type(of: brightness), Pixel.max, Int8.min)
aliasProbe.report()
```

```
UInt8 255 -128
alias-constant: 1 evaluations, 2 uses -> immediately
```

`type(of: brightness)` printed `UInt8`, so the alias did not create a new type. `Pixel.max` came out 255 and `Int8.min` came out -128. The `Probe` showed a single evaluation, and because the constant was set up before the use, the moment is `immediately`. An alias is only a convenience at the level of names; it adds nothing at run time.

Knowing the limit properties is the basis of conversion and overflow decisions. You can check ahead of time whether a value will fit by comparing it with `max`; that way you never run a conversion that does not fit.

**LB20.** The limit values were shown with `UInt8` and `Int8`; the limits of the other integer types lie outside this lesson.

## Converting a Fraction to an Integer

There is not a single way to turn a `Double` value into an `Int`, and three common ways give different results. `Int(value)` drops the fractional part, that is, it truncates toward zero. `value.rounded()` rounds to the nearest whole value; on an exact midpoint it goes away from zero. `value.rounded(.down)` always goes down to the smaller one. The three either produce a `Double` first or give an `Int` directly; which way you choose changes the result for negative numbers.

The block below converts one number in three ways. The value to convert comes from a `Probe`, and the function is called with a positive and a negative value.

```swift
// Rounding.swift — converting a fraction to an integer
func convert(_ value: Double, probe: Probe) -> (Int, Int, Int) {
    let source = probe.eval(value)
    return (Int(source), Int(source.rounded()), Int(source.rounded(.down)))
}
let roundProbe = Probe("rounding")
print(roundProbe.use { convert(2.5, probe: roundProbe) })
print(roundProbe.use { convert(-2.5, probe: roundProbe) })
roundProbe.report()
```

```
(2, 3, 2)
(-2, -3, -3)
rounding: 2 evaluations, 2 uses -> on-every-use
```

For positive `2.5` the three ways gave `(2, 3, 2)`: truncation 2, rounding to nearest 3, rounding down 2. For negative `-2.5` the result was `(-2, -3, -3)`. Truncation moves toward zero, so it gave `-2`; rounding to nearest goes away from zero at the exact midpoint, so it gave `-3`; and rounding down always goes to the smaller one, so it also gave `-3`. For a positive number, truncation and rounding down give the same result; for a negative number they part ways. The `Probe` showed two evaluations for two calls, so the moment reads `on-every-use`.

When you choose the conversion way, what you should ask is whether negative values will also arrive. If they will not, two of the three ways do the same job; if they will, the rule of each is a separate decision.

**LB21.** Rounding was measured with `2.5` and `-2.5`; the other rounding rules (`.up`, `.toNearestOrEven`) lie outside this lesson.

## Decimal Equality

Binary floating-point numbers cannot hold some simple fractions of the base-ten system exactly. `0.1`, `0.2` and `0.3` are the best-known of these: each is rounded to the nearest representable value. Adding two numbers and comparing the result with a third using `==` can therefore give an unexpected result. When you compare decimal numbers, you choose an acceptable difference limit instead of exact equality.

The block below measures this case. The first term of the sum comes from a `Probe`. The block prints the sum, the result of exact equality, and whether the difference stays under a small limit.

```swift
// Equality.swift — decimal equality
func sum(probe: Probe) -> Double {
    probe.eval(0.1) + 0.2
}
let equalProbe = Probe("decimal-sum")
let total = equalProbe.use { sum(probe: equalProbe) }
print(total)
print(total == 0.3)
print(abs(total - 0.3) < 0.000001)
equalProbe.report()
```

```
0.30000000000000004
false
true
decimal-sum: 1 evaluations, 1 uses -> on-first-use
```

The sum printed `0.30000000000000004`, exact equality gave `false`, and the comparison with a small limit came out `true`. Because `0.1` and `0.2` cannot be written exactly in base two, their sum did not meet `0.3` at the same value; the difference is in the last digit. That is why `==` is not a reliable test for computed decimal values. The measurement showed a single evaluation: the first term was taken from the `Probe` once, and the later comparisons did not recompute it, so the moment is `on-first-use`.

The size of the limit is chosen to fit the job; here `0.000001` was a sufficient measure. For values like money, where the cent matters, working with integer units is safer than a decimal type.

Choose the rounding rule by intent, not by looking at the result. If you want to cut a sum of money or a counter downward, you write `.down`; if you want the nearest value, `rounded()`; if you want only the whole part, `Int(value)`. Treating the three as the same causes a wrong result on the day the first negative value arrives. If the `Double` being converted is outside the integer range, `Int(value)` stops the program; this lesson does not run that case.

The same principle holds for decimal equality: compare the result with a limit that fits what it should be, and do not rely on exact equality. Putting the limit into a named constant keeps the same measure consistent in more than one place.

**LB22.** Equality was measured with `Double`; the result of the same sum with types such as `Float` and `Decimal` lies outside this lesson.

## The Bounding Measurement

Look at the `unused-element` line: an element was defined inside a tuple, evaluated once, and never read. The rule says `immediately`, because the evaluation happened before the use and there was no use at all. That means a tuple element is evaluated even when it is not read; `never` appears only when the expression is never evaluated. This line also shows why the measurement condition asks for "at least two uses": with zero uses, the rule cannot tell a wasted evaluation from a real `immediately`.

**LB23.** Zero uses was left on this line on purpose; no other observation in the lesson carries zero uses.

## Summary

- `Int`, `Double`, `Bool` and `String` are the four basic types; types are not mixed on their own.
- Conversion is explicit: `Double(count)` is an expression and is evaluated on the line where it is defined.
- `0.1 + 0.2 == 0.3` gives `false` because of the floating-point representation.
- Tuple elements are evaluated from left to right, even when they are not read.
- This lesson's moment is **immediately** (a conversion expression, once, on the `let` line).

## Next Step

`String` appeared in this lesson only by name. The next lesson goes down into strings: what is a character, why do `count` and `utf8.count` part ways, and how many times is an expression placed inside a string set up?
