Basics: Constants, Variables and Types

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every Swift value has a type, and every name that holds one is declared either a constant or a variable. Those two facts — explicit mutability and a value’s type — are the foundation the rest of the language builds on.

Constants and Variables

let declares a constant: its value is set once and cannot change. var declares a variable:

let maximumLoginAttempts = 10
var currentLoginAttempt = 0

currentLoginAttempt += 1
// maximumLoginAttempts += 1   // error: cannot assign to a `let` constant

var x = 0, y = 0, z = 0        // multiple declarations on one line

Prefer let by default; reach for var only when a value genuinely needs to change. The compiler flags a var that is never mutated after its initial assignment as a candidate for let.

Type Annotations and Type Inference

A type annotation spells out a name’s type explicitly; without one, Swift infers the type from the initial value:

var welcomeMessage: String            // annotation -- no initial value yet
welcomeMessage = "Hello"

var red, green, blue: Double          // one annotation covering three names

let inferredInt = 42                  // inferred as Int
let inferredDouble = 3.14             // inferred as Double

A stored constant or variable must have a value before it is read, and (unlike a var) a let may be assigned exactly once, at any point before its first use — it need not be initialized at its declaration.

Printing and String Interpolation

let name = "Ada"
let attempts = 3

print("Hello, \(name)!")                       // "Hello, Ada!"
print("Attempt \(attempts) of \(attempts * 2)") // expressions are allowed inside \(...)
print(name, attempts, separator: ", ", terminator: "\n")

print(_:separator:terminator:) writes to standard output; terminator defaults to a newline and can be set to "" to suppress it. String interpolation (\(…​)) is covered in full, including custom String(reflecting:)/CustomStringConvertible formatting, in Strings and Characters.

Integer Types

Swift ships fixed-width signed and unsigned integer types in 8, 16, 32 and 64-bit sizes, plus Int/UInt sized to the platform’s native word:

Type Width Range

Int8 / UInt8

8 bits

-128…​127 / 0…​255

Int16 / UInt16

16 bits

-32768…​32767 / 0…​65535

Int32 / UInt32

32 bits

~ ±2.1 x 109 / 0…​~4.3 x 109

Int64 / UInt64

64 bits

~ ±9.2 x 1018 / 0…​~1.8 x 1019

Int / UInt

32 or 64 bits

same as Int32/UInt32 or Int64/UInt64, matching the platform’s native word size

let minValue = UInt8.min      // 0
let maxValue = UInt8.max      // 255

let int8Max = Int8.max        // 127

Use Int unless you specifically need a fixed-width or unsigned type — for a loop counter, an array index, or a count, even when the value can never be negative. Int is what type inference produces for an integer literal, it interoperates with every standard-library API without a cast, and its width already matches the platform’s native integer on every current Apple and Linux target. Reach for UInt only for a value that mirrors an unsigned type in an external API, and for a fixed-width type (Int32, UInt8, …​) only for on-the-wire formats, bit manipulation, or interop that needs a specific width.

Floating-Point Types and Choosing Between Them

Double (64-bit, at least 15 decimal digits of precision) and Float (32-bit, at least 6 decimal digits) are both IEEE 754 binary floating point. Prefer Double: it is the type Swift infers for a floating-point literal, and its extra precision costs little on modern hardware. Reach for Float only under real memory or interop pressure (large buffers of graphics data, an API that requires it).

let pi: Double = 3.14159
let ratio: Float = 1.0 / 3.0

print(0.1 + 0.2 == 0.3)              // false -- binary floating point, same trap as every other language
print(Double.infinity, Double.nan)   // special values exist, as IEEE 754 requires
print(Double.nan == Double.nan)      // false -- compare with .isNaN instead

Numeric Literals

let decimal = 17
let binary = 0b10001          // 17
let octal = 0o21              // 17
let hexadecimal = 0x11        // 17

let hexFloat = 0xC.3p0        // 12.1875 -- hexadecimal floating-point, base-16 mantissa with a required `p` exponent

let paddedDouble = 000123.456
let justOverOneMillion = 1_000_000.000_000_1   // `_` is an ignored separator anywhere in a literal

Numeric literals carry no type of their own — they are inferred from context (Int for an integer literal, Double for a floating-point one) unless an annotation or an existing value says otherwise.

Numeric Type Conversion

Swift never converts numeric types implicitly, even between two integer types that could hold every value of each other — every conversion is written out with an initializer:

let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
// let twoThousandAndOne = twoThousand + one          // error: UInt16 and UInt8 don't mix
let twoThousandAndOne = twoThousand + UInt16(one)      // explicit conversion

let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine       // Int -> Double, also explicit

let integerPi = Int(pi)          // 3 -- Double -> Int truncates toward zero, it does not round

This is deliberate: a language with implicit numeric conversions hides overflow and precision-loss bugs behind innocuous-looking arithmetic; Swift makes every narrowing or cross-type conversion a visible call site.

Type Aliases

typealias gives an existing type an additional name, useful for a domain-specific name or for a long generic type spelled out repeatedly:

typealias AudioSample = UInt16

var maxAmplitudeFound = AudioSample.min   // 0 -- AudioSample is just another name for UInt16

typealias JSONDictionary = [String: Any]

A type alias introduces no new type — AudioSample and UInt16 are completely interchangeable everywhere, including in overload resolution.

Booleans

let orangesAreOrange = true
let turnipsAreDelicious = false

if turnipsAreDelicious {
    print("Mmm, tasty turnips!")
} else {
    print("Eww, turnips are horrible.")
}

Bool is a genuine, distinct type — unlike C, 1 and 0 are not interchangeable with true/false, and if 1 { …​ } does not compile.

Tuples

A tuple groups multiple values, of any types, into a single compound value; the individual values need not be the same type:

let http404Error = (404, "Not Found")            // type is (Int, String)

let (statusCode, statusMessage) = http404Error   // decomposed into two constants
print("The status code is \(statusCode)")
print("The status message is \(statusMessage)")

let (justTheCode, _) = http404Error              // `_` ignores a component you don't need
print("The status code is \(justTheCode)")

print("The status code is \(http404Error.0)")    // access by index...
let http200Status = (statusCode: 200, description: "OK")
print("The status code is \(http200Status.statusCode)")   // ...or by name, when named at creation

Tuples are a lightweight way to return multiple values from a function without defining a named type — see Functions — and appear again as a pattern-matching target in Pattern Matching.

Assertions, Preconditions, and Fatal Errors

assert and precondition both check a condition at run time and stop execution if it is false; they differ only in when the check runs:

let age = -3

// assert: checked only in debug builds (-Onone); compiled out entirely in -O release builds.
assert(age >= 0, "A person's age cannot be less than zero.")

// precondition: checked in both debug AND release builds -- use it for a violated
// condition that could genuinely corrupt program state if execution continued.
func indexIsValid(_ index: Int, count: Int) -> Bool { index >= 0 && index < count }
func element(at index: Int, in array: [Int]) -> Int {
    precondition(indexIsValid(index, count: array.count), "Index out of range")
    return array[index]
}

// fatalError: unconditionally stops execution, with no check -- for a code path that
// must never be reached, such as an unimplemented `required init` or an exhaustive
// switch's supposedly unreachable default.
func processedValue(for input: Int) -> Int {
    switch input {
    case let n where n >= 0: return n
    default: fatalError("Negative input is not supported by this API")
    }
}

Use assert for conditions expensive enough that checking them in shipping code is wasteful, precondition for conditions cheap enough (or important enough) to check everywhere, and fatalError for a path that signals a programming error rather than a recoverable one — none of the three is a substitute for throws/Result, which model expected, recoverable failure and are covered in Error Handling.

See Also

  • Optionals — the type that represents the absence of one of these values.

  • Operators — the operators these types support.

  • Strings and Characters — String in depth, including interpolation’s formatting rules.

  • Control Flow — if/switch as used above.