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 |
|---|---|---|
|
8 bits |
-128…127 / 0…255 |
|
16 bits |
-32768…32767 / 0…65535 |
|
32 bits |
~ ±2.1 x 109 / 0…~4.3 x 109 |
|
64 bits |
~ ±9.2 x 1018 / 0…~1.8 x 1019 |
|
32 or 64 bits |
same as |
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 —
Stringin depth, including interpolation’s formatting rules. -
Control Flow —
if/switchas used above.