top of page

Swift Study Notes

What Is Type Safety?

💡 The most important idea

Type safety means that Swift checks whether values are being used as the correct types before the application runs.

If the compiler can prove that an operation is invalid, Swift refuses to build the program.

Instead of allowing a predictable type error to become a runtime crash, Swift attempts to stop the mistake during compilation.

Swift is commonly described as a safe, fast and expressive programming language.

One of the most important parts of that description is type safety.

Every value in Swift has a type.

An integer is an Int.

A piece of text is a String.

A user model may be a User.

An optional user is an Optional<User>.

The compiler tracks these types and verifies that they are used in valid ways.

If we attempt to assign a String to a property that requires an Int, Swift does not wait until the application is running before discovering the mistake.

let age: Int = "Forty-two"

The program does not compile.

The compiler already knows that a String cannot be stored where an Int is required.

What Does Safety Mean?

Safety does not mean that a Swift application can never crash.

Programs can still contain logic errors.

They can still force unwrap nil.

They can still access an invalid collection index.

They can still call fatalError().

They can still receive unexpected data from a server.

Swift’s safety features attempt to move as many predictable failures as possible from runtime into compile time.

Swift’s compiler attempts to prove that the operations described by the source code are valid before those operations are allowed to run.

If the compiler cannot establish that an operation is permitted by the type system, the code usually fails to compile.

Compile Time and Runtime

To understand type safety, we first need to distinguish between compile time and runtime.

Compile Time

Compile time is the period during which the Swift compiler analyses the source code and transforms it into executable machine code.

During compilation, Swift checks rules involving:

  • Types.
  • Function calls.
  • Protocol conformances.
  • Access control.
  • Initialisation.
  • Generic constraints.
  • Actor isolation.
  • Ownership relationships.
  • Exhaustive control flow.

Runtime

Runtime is the period during which the compiled program is actually executing.

This is when the application:

  • Responds to user input.
  • Downloads network data.
  • Reads databases.
  • Creates objects in RAM.
  • Updates the user interface.
  • Performs calculations.
  • Handles events.

Type safety attempts to ensure that the compiled instructions agree with the types of values the program will manipulate at runtime.

Swift Is a Compiled Language

Swift is a compiled programming language.

The source code we write is not interpreted line by line as plain text while the application runs.

The compiler analyses the program and produces native executable instructions for the target platform.

Swift source code
        │
        ▼
Swift compiler
        │
        ▼
Type checking and optimisation
        │
        ▼
Native machine code
        │
        ▼
Running application

Because the compiler analyses the program before execution, it can reject many invalid operations in advance.

A Simple Type-Safety Example

let score: Int = 100

This is valid.

The declared type and the supplied value agree.

This is not valid:

let score: Int = "One hundred"

A String cannot be stored as an Int.

Swift reports a compile-time error.

The invalid program never becomes a running application.

Type Inference Is Still Type Safety

Swift does not always require us to write the type explicitly.

let score = 100

The compiler infers that score is an Int.

Type inference does not mean that the value has no type.

It means the compiler determined the type from the expression.

let name = "Milo"       // String
let score = 100         // Int
let progress = 0.75     // Double
let isReady = true      // Bool

The types remain known and checked.

Objective-C and Runtime Flexibility

Objective-C has a type system, but it also relies heavily on a dynamic runtime.

Messages can be sent to objects whose exact concrete behaviour may not be fully verified until the program is running.

This flexibility made Objective-C powerful.

It also allowed some errors to survive compilation and fail only at runtime.

A familiar Objective-C failure was:

unrecognized selector sent to instance

This occurred when an object received a message corresponding to a method it did not implement.

Conceptually, the code expected the object to support one structure or capability, but the runtime object did not match that expectation.

The application could then terminate with an exception.

A Simplified Objective-C Example

id object = someValue;
[object launchRocket];

The type id can refer to an Objective-C object without specifying a particular concrete class.

The compiler may allow the message to be sent.

At runtime, the Objective-C messaging system asks the object to respond to launchRocket.

If the object does not implement that selector, the application may crash.

The Swift Approach

Swift generally requires the compiler to know that the operation is valid.

protocol RocketLaunching {
    func launchRocket()
}

func beginLaunch(
    using launcher: RocketLaunching
) {
    launcher.launchRocket()
}

The compiler knows that every value accepted by beginLaunch must conform to RocketLaunching.

The required method is part of that contract.

Passing an unrelated value fails at compile time.

struct CoffeeMachine {
    func makeCoffee() {
        print("Coffee ready")
    }
}

let machine = CoffeeMachine()

// Compile-time error:
// beginLaunch(using: machine)

The CoffeeMachine does not conform to RocketLaunching.

The invalid call cannot become a runtime type mismatch.

Types Describe the Shape of Values

A type tells Swift what a value contains and which operations are available.

struct Launch {
    let name: String
    let flightNumber: Int
    let isUpcoming: Bool
}

The compiler knows that a Launch contains:

  • A String named name.
  • An Int named flightNumber.
  • A Bool named isUpcoming.

The compiler also knows that Launch does not contain an arbitrary property named fuelColour.

let launch = Launch(
    name: "Explorer",
    flightNumber: 42,
    isUpcoming: true
)

// Compile-time error:
// print(launch.fuelColour)

The code is rejected before the application runs.

Types and Memory

When a program runs, values occupy memory in RAM.

Some storage may be associated with stack frames.

Other storage may be dynamically managed on the heap.

The type system describes how the program is permitted to interpret and manipulate those values.

For example, Swift knows that this value is an Int:

let flightNumber: Int = 42

The compiler generates instructions appropriate for an Int.

It does not allow the same value to be treated as an unrelated String merely because both values occupy memory.

Type safety prevents code from arbitrarily interpreting one kind of value as another incompatible kind of value.

Stack and Heap Storage Do Not Remove Type Safety

Whether a value is represented using stack storage, heap storage or an optimised combination does not change its language-level type.

struct Coordinates {
    let x: Double
    let y: Double
}

final class Rocket {
    let name: String

    init(name: String) {
        self.name = name
    }
}

Coordinates has value semantics.

Rocket has reference semantics.

The compiler still verifies the valid operations for both.

A class instance being stored on the heap does not mean that Swift loses compile-time type safety.

The exact lifetime and address may depend upon runtime behaviour, but the allowed operations remain governed by the type system.

Compile-Time Safety Is Not the Same as Memory Location

The compiler may not know the exact runtime address at which an object will be allocated.

It can still know the object’s type.

let rocket: Rocket = Rocket(name: "Explorer")

The heap allocator chooses storage while the application runs.

The compiler already knows that the reference is a Rocket and which members can be accessed through it.

Type safety and allocation location are related to program correctness, but they are not the same concept.

Optionals Provide Type Safety for Missing Values

Swift does not normally allow the absence of a value to hide inside an ordinary non-optional type.

let name: String = "Milo"

This value must contain a String.

If the value may be absent, that possibility must be represented in the type.

let name: String?

This is an Optional<String>.

It can be:

.some("Milo")
.none

The compiler requires us to acknowledge that the value may not exist before using it as a String.

if let name {
    print(name.uppercased())
}

This prevents many null-pointer-style errors that could otherwise appear at runtime.

Force Unwrapping Opts Out of Optional Safety

Swift allows the programmer to override optional checking using the force unwrap operator.

let name: String? = nil
let value = name!

This compiles because the programmer has made an assertion that the value will exist.

If the assertion is wrong, the application traps at runtime.

The type system provided a safe mechanism.

The programmer deliberately bypassed it.

Function Parameters Are Type Checked

func displayFlightNumber(
    _ number: Int
) {
    print(number)
}

The compiler verifies every call.

displayFlightNumber(42)

This is valid.

// Compile-time error:
// displayFlightNumber("Forty-two")

The caller cannot accidentally supply a String.

Function Return Types Are Checked

func nextFlightNumber() -> Int {
    42
}

The compiler verifies that every path returning from the function produces an Int.

This is invalid:

func nextFlightNumber() -> Int {
    "Forty-two"
}

The declared contract says the function returns an Int.

The implementation attempts to return a String.

Swift rejects the mismatch.

All Returning Code Paths Must Agree

func launchStatus(
    isReady: Bool
) -> String {
    if isReady {
        return "Ready"
    }

    return "Waiting"
}

Every path returns a String.

This does not compile:

func launchStatus(
    isReady: Bool
) -> String {
    if isReady {
        return "Ready"
    }

    // Missing return value
}

The compiler knows that the false branch could reach the end of the function without producing the promised String.

Definite Initialisation

Swift requires stored properties to have valid values before an instance finishes initialising.

struct Rocket {
    let name: String
    let flightNumber: Int
}

Swift synthesises an initializer requiring both values.

let rocket = Rocket(
    name: "Explorer",
    flightNumber: 42
)

An incomplete value cannot be created.

// Compile-time error:
// let rocket = Rocket(name: "Explorer")

The compiler prevents an instance whose required properties were never initialised.

Constants Cannot Be Mutated

A value declared using let cannot be reassigned.

let flightNumber = 42

// Compile-time error:
// flightNumber = 43

This is another guarantee enforced before runtime.

The compiler protects the immutability promised by the declaration.

Access Control Is Checked at Compile Time

final class APIClient {
    private let token = "secret"
}

Code outside the permitted scope cannot access the private property.

let client = APIClient()

// Compile-time error:
// print(client.token)

Access-control rules protect implementation boundaries.

Protocol Conformance Is Verified

protocol Launching {
    func launch()
}

A conforming type must provide the required functionality.

struct Rocket: Launching {
    func launch() {
        print("Launching")
    }
}

This does not compile:

struct BrokenRocket: Launching {
    // Missing launch()
}

The compiler prevents an incomplete conformance from entering the program.

Generic Constraints Are Checked

Generics allow one implementation to work with several types while preserving type information.

func areEqual<T: Equatable>(
    _ first: T,
    _ second: T
) -> Bool {
    first == second
}

The constraint states that T must conform to Equatable.

The compiler verifies that every caller satisfies the requirement.

areEqual(10, 20)
areEqual("A", "B")

A non-Equatable type cannot be supplied.

The function is reusable without abandoning type safety.

Generic Relationships Are Preserved

func choose<T>(
    _ first: T,
    _ second: T,
    useFirst: Bool
) -> T {
    useFirst ? first : second
}

The same generic type T appears in both parameters and the return type.

This expresses a relationship:

Both arguments must have the same type, and the returned value will have that same type.

The compiler preserves that relationship for every call.

Exhaustive Switch Statements

When switching over an enum, Swift requires every possible case to be handled.

enum LaunchState {
    case waiting
    case launching
    case completed
}
func message(
    for state: LaunchState
) -> String {
    switch state {
    case .waiting:
        "Waiting"

    case .launching:
        "Launching"

    case .completed:
        "Completed"
    }
}

If a case is omitted, the compiler reports an error.

This becomes especially valuable when a new enum case is added.

Every switch that must consider that case can be identified by the compiler.

Pattern Matching Is Type Checked

let result: Result<Data, Error>

switch result {
case .success(let data):
    print(data.count)

case .failure(let error):
    print(error)
}

The associated value extracted from .success is known to be Data.

The value extracted from .failure is known to satisfy Error.

The compiler verifies the patterns and their bound values.

Error Handling Is Type Checked

A throwing function explicitly communicates that it can fail.

func fetchLaunch() async throws -> Launch

The caller must acknowledge the possibility of an error.

let launch = try await fetchLaunch()

Or handle it:

do {
    let launch = try await fetchLaunch()
    print(launch.name)
} catch {
    print(error)
}

The compiler does not allow a throwing call to be treated as though failure were impossible.

Memory Exclusivity

Swift enforces exclusive access to memory during mutation.

Code cannot safely perform overlapping incompatible modifications to the same value.

func increment(
    _ value: inout Int
) {
    value += 1
}

Swift tracks the exclusive write access created by inout.

Conflicting access is rejected at compile time where possible and may be checked dynamically where required.

This protects the program from certain forms of undefined mutation behaviour.

Actor Isolation

Swift concurrency introduces actor isolation to protect mutable state from unsynchronised access.

actor LaunchCounter {
    private var count = 0

    func increment() {
        count += 1
    }

    func currentCount() -> Int {
        count
    }
}

Code outside the actor cannot synchronously access its isolated state.

let counter = LaunchCounter()

await counter.increment()
let count = await counter.currentCount()

The compiler requires the isolation boundary to be respected.

Main-Actor Isolation

UI-facing models can be isolated to the main actor.

@MainActor
final class LaunchModel {
    var status = "Waiting"
}

The compiler checks access to the isolated state.

This helps prevent accidental updates from an inappropriate concurrency context.

Sendable Checking

Swift uses Sendable to describe values that can safely cross concurrency boundaries.

struct Launch: Sendable {
    let name: String
    let flightNumber: Int
}

The compiler can diagnose unsafe transfers involving mutable reference state.

This provides another layer of compile-time reasoning about concurrent code.

Ownership and Noncopyable Types

Modern Swift includes ownership features that allow the language to express whether values are borrowed, consumed or copied.

These tools help prevent invalid uses of values whose ownership has moved elsewhere.

Noncopyable types can represent resources that must not be duplicated implicitly.

The compiler can enforce those ownership rules before runtime.

Capture Checking in Escaping Closures

Escaping closures require explicit references to instance members in situations where capture behaviour should be visible.

final class LaunchController {
    let name = "Explorer"

    func callback() -> () -> Void {
        {
            print(self.name)
        }
    }
}

The explicit self makes the capture relationship visible.

Swift also checks whether escaping closures capture values that cannot safely escape their scope.

Result Builder Validation

SwiftUI uses result builders to transform declarative view syntax into strongly typed view structures.

var body: some View {
    VStack {
        Text("Rocket")
        ProgressView()
    }
}

The code may look like a loose collection of interface instructions.

It is still type checked.

The expressions must produce valid View-conforming results accepted by the builder.

Opaque Return Types Preserve Type Information

func makeTitle() -> some View {
    Text("Rocket")
}

The concrete return type is hidden from the caller.

It is still fixed and known to the compiler.

Opaque types preserve more compile-time information than an arbitrary runtime value would provide.

Existential Types Are Still Type Checked

let launcher: any Launching

An existential value can contain different conforming concrete types at runtime.

The compiler still restricts operations to those available through the protocol interface.

Runtime flexibility does not mean abandoning all type safety.

Safe Casting

Swift provides conditional casting using as?.

if let rocket = value as? Rocket {
    print(rocket.name)
}

The cast returns an optional.

If the runtime value is not a Rocket, the result is .none.

This lets the program handle the mismatch safely.

Forced Casting Opts Into Runtime Failure

let rocket = value as! Rocket

The forced cast asserts that the runtime value is definitely a Rocket.

If the assertion is wrong, the program traps.

Like force unwrapping, this is an explicit decision to bypass a safer checked operation.

Array Element Types Are Enforced

var flightNumbers: [Int] = [1, 2, 3]

The array can store Int values.

flightNumbers.append(4)

This does not compile:

// flightNumbers.append("Five")

The compiler prevents a mixed and unexpected element type.

Dictionary Key and Value Types Are Enforced

var launchNames: [Int: String] = [
    1: "Explorer",
    2: "Voyager"
]

The key must be an Int.

The value must be a String.

Invalid additions are rejected.

Key Paths Are Type Safe

struct Launch {
    let name: String
    let flightNumber: Int
}

let namePath: KeyPath<Launch, String> = \.name

The key path records both the root type and the value type.

The compiler verifies that Launch.name really is a String.

Type Safety Does Not Eliminate Runtime Information

Some decisions can only be made while the program runs.

A server may return malformed JSON.

A user may enter invalid data.

A network request may fail.

An existential value may contain one of several conforming concrete types.

An optional may be .none.

Type safety does not predict every external event.

It ensures that the program handles those possibilities using valid typed operations.

Swift Still Has Runtime Errors

The following operations can still cause runtime failure.

Force Unwrapping nil

let name: String? = nil
print(name!)

Forced Cast Failure

let value: Any = "Rocket"
let number = value as! Int

Invalid Collection Index

let values = [1, 2, 3]
print(values[10])

Invalid Unowned Reference

Accessing an unowned reference after its object has been deallocated causes a runtime trap.

Explicit Fatal Errors

fatalError("This state should be impossible")

Failed Preconditions

precondition(count >= 0)

Swift provides safety, but it also allows programmers to state assumptions.

If those assumptions are false, runtime failure may be the chosen result.

Undefined Behaviour and Memory Safety

Languages such as C allow direct pointer arithmetic and many low-level memory operations.

Used incorrectly, these operations can interpret memory as the wrong type, access released storage or write beyond an allocated region.

Swift removes many of these possibilities from ordinary safe code.

Unsafe operations still exist when systems programming requires them.

UnsafePointer<UInt8>
UnsafeMutableRawPointer

The word Unsafe is deliberate.

Once we enter unsafe APIs, the compiler cannot provide the same guarantees.

Major Compile-Time Safety Features in Swift

It is difficult to produce a permanently complete list because Swift continues to evolve.

However, the major compile-time safety mechanisms include:

Safety Feature What It Protects
Static type checking Values are used as compatible types.
Type inference Types remain known even when not written explicitly.
Optional checking Possibly absent values must be acknowledged.
Definite initialisation Required properties are initialised before use.
Immutability checking let values cannot be reassigned.
Function signature checking Arguments and return values match declared types.
Protocol conformance checking Conforming types implement required capabilities.
Generic constraints Generic operations use only supported behaviour.
Exhaustive switch checking All enum states are handled.
Access control Private implementation details remain protected.
Memory exclusivity Conflicting access to mutable memory is prevented.
Error-propagation checking Throwing operations must be acknowledged.
Actor isolation Actor-protected state is accessed through valid boundaries.
Sendable checking Unsafe transfers across concurrency domains are diagnosed.
Ownership checking Borrowed, consumed and noncopyable values follow ownership rules.
Closure escape checking Values do not improperly outlive their valid scope.
Result-builder checking Declarative builder expressions produce valid typed results.
Pattern-matching checking Patterns agree with the matched value’s type.
Key-path checking Key paths preserve root and property types.
Safe conditional casting Runtime type uncertainty can be represented using optionals.

Not Every Safety Check Happens Only at Compile Time

Some Swift protections combine compile-time reasoning with runtime checks.

Array bounds are a good example.

The compiler knows that an array index must be an Int.

It may not know whether the array will contain five or fifty values at runtime.

The actual bounds therefore need to be checked while the program runs.

Memory exclusivity may also require runtime enforcement when static analysis cannot prove the full access pattern.

Swift safety is a combination of:

  • Compile-time rejection.
  • Runtime checks.
  • Explicit programmer assertions.

What Are We Expected to Know About Swift?

In interviews, documentation and technical conversations, Swift is often described using a collection of important characteristics.

Swift Characteristic Meaning
Compiled Swift source code is compiled into native executable code.
Statically typed Types are known and checked during compilation.
Type safe Invalid operations between incompatible types are rejected.
Memory safe Ordinary Swift prevents many invalid memory operations.
High performance Swift is designed to produce efficient native code.
Expressive Modern syntax allows complex ideas to be written clearly.
Multi-paradigm Swift supports several styles of programming.
Protocol-oriented Protocols and protocol extensions can organise reusable behaviour.
Object-oriented Classes support identity, inheritance and shared reference state.
Functional Functions, closures and value transformations are first-class tools.
Imperative Swift supports explicit sequences of commands and mutations.
Declarative Frameworks such as SwiftUI allow results to be described from state.
Event-driven iOS applications respond to user actions and external events.
Reactive Observation systems propagate changes to dependent code.
Generic Reusable algorithms preserve concrete type relationships.
Concurrent Swift provides tasks, actors, async/await and structured concurrency.
ARC-managed Class-instance lifetimes are managed through reference counting.
Value-oriented Structures and enums provide predictable value semantics.
Interoperable Swift can work alongside Objective-C and Apple frameworks.
Open source The language, compiler and many supporting tools are developed openly.
Cross-platform Swift can be used beyond Apple platforms, including server and systems environments.

Is Swift as Fast as C++?

Swift is designed for high performance and can produce native machine code with performance suitable for demanding applications.

In some workloads, well-optimised Swift can perform competitively with C++.

In other workloads, the results may differ because of:

  • Allocation patterns.
  • ARC traffic.
  • Copy-on-write behaviour.
  • Generic specialisation.
  • Optimisation settings.
  • Algorithm choice.
  • Library implementation.
  • Compiler maturity.

It is better to say:

Swift is designed to offer high-level readability while still producing high-performance native code.

Performance should be measured for the actual workload rather than assumed from the language name alone.

High-Level Readability

Swift provides abstractions that are easier to read and safer to use than many equivalent low-level operations.

Consider filtering a collection.

let upcomingLaunches = launches.filter(\.isUpcoming)

This is expressive and concise.

The compiler still knows the types involved.

High-level syntax does not require abandoning native performance or static checking.

What Is a Programming Paradigm?

A programming paradigm is a general style or philosophy for organising and expressing code.

It influences how a developer thinks about the problem.

Swift is multi-paradigm because it supports several of these styles.

Imperative Programming

Imperative code tells the computer which instructions to perform.

var count = 0
count += 1
print(count)

The code changes state through a sequence of commands.

Object-Oriented Programming

Object-oriented code organises state and behaviour around objects with identity.

final class Rocket {
    var fuelLevel = 100

    func launch() {
        fuelLevel -= 10
    }
}

Protocol-Oriented Programming

Protocol-oriented code organises behaviour around capabilities.

protocol Launchable {
    func launch()
}

Several unrelated types can provide the same capability.

Functional Programming

Functional code often transforms values using functions rather than relying upon shared mutable state.

let names = launches
    .filter(\.isUpcoming)
    .map(\.name)

Event-Driven Programming

Event-driven code responds when something happens.

func buttonPressed() {
    beginLaunch()
}

Network responses, notifications, database updates and user actions are all events.

Reactive Programming

Reactive programming describes how changes propagate to dependent parts of the program.

In SwiftUI, a view can observe model state and be reevaluated when that state changes.

Declarative Programming

Declarative code describes the desired result for the current state.

var body: some View {
    if isLaunching {
        ProgressView()
    } else {
        Text("Ready")
    }
}

The code describes what should be visible rather than manually mutating an existing hierarchy step by step.

The Paradigms Work Together

A single feature may use several paradigms.

User presses a button
        │
        ▼
Event-driven handler
        │
        ▼
Imperative model operation
        │
        ▼
Asynchronous network request
        │
        ▼
Observable state changes
        │
        ▼
Reactive dependency update
        │
        ▼
Declarative SwiftUI description

These paradigms complement one another.

They are not competing for permission to be the only style used in the application.

Why Type Safety Matters in Interviews

When an interviewer asks whether Swift is type safe, they are not only asking whether this assignment compiles:

let age: Int = "42"

They may be asking whether you understand Swift’s wider design.

A strong answer should mention that:

  • Types are checked statically.
  • Function inputs and outputs form compile-time contracts.
  • Optionals represent absence explicitly.
  • Generics preserve type relationships.
  • Protocol conformances are verified.
  • Switches over enums are exhaustive.
  • Concurrency isolation is increasingly checked by the compiler.
  • Unsafe or forced operations can still introduce runtime traps.

Every Compiler Error Is Not an Obstacle

It can be frustrating when the Swift compiler refuses to build code that appears almost correct.

However, many compiler errors represent a bug that has been prevented from reaching a user.

Cannot convert value of type 'String'
to expected argument type 'Int'

This message is not the compiler being difficult.

It is the compiler proving that the current program contradicts its own type declarations.

The compiler is not fighting against the programmer.

It is refusing to produce an executable program whose typed contracts have already been broken.

Type Safety Improves Refactoring

Suppose a property changes from String to Date.

struct Launch {
    let launchDate: Date
}

Any code still treating the property as a String can be identified by the compiler.

This makes large refactoring operations safer.

The type system acts as a map of the assumptions distributed throughout the application.

Type Safety Improves Documentation

A function signature communicates what the function accepts and returns.

func fetchNextLaunch() async throws -> Launch

Before reading the implementation, we already know:

  • The function is asynchronous.
  • It can throw an error.
  • It returns a Launch.

The type system is part of the documentation.

Type Safety Improves Autocomplete

Because the compiler and development tools know a value’s type, Xcode can suggest valid members.

launch.

Autocomplete can offer properties and methods belonging to Launch.

It does not need to guess every possible operation in the program.

Type Safety Improves Optimisation

Knowing concrete types can help the compiler optimise code.

The compiler may be able to:

  • Inline functions.
  • Specialise generic implementations.
  • Remove dynamic dispatch.
  • Eliminate temporary values.
  • Choose efficient storage layouts.
  • Remove unnecessary retains and releases.

Type information is valuable for both correctness and performance.

Dynamic Behaviour Still Exists in Swift

Swift is statically typed, but it can still support runtime polymorphism and dynamic behaviour.

Examples include:

  • Class inheritance.
  • Protocol existentials.
  • Dynamic casting.
  • Objective-C interoperability.
  • Reflection.
  • Runtime-loaded data.

The difference is that Swift attempts to place typed boundaries around those operations.

Any Does Not Remove All Type Safety

let value: Any = "Rocket"

The variable can contain a value of any type.

However, Swift does not let us use it as a String without checking or asserting the type.

if let name = value as? String {
    print(name.uppercased())
}

The uncertainty is explicit.

Type Safety Is Not Business-Logic Safety

This code may be perfectly type safe:

func calculateDiscount(
    price: Double
) -> Double {
    price * 10
}

Every type is valid.

The business logic is probably wrong.

Type safety can prove that a Double is returned.

It cannot prove that the discount formula matches the company’s intention unless those rules have been encoded into the program’s types and tests.

Making Invalid States Harder to Represent

A strong type system allows us to design models that exclude invalid combinations.

Consider:

struct DownloadState {
    var isLoading: Bool
    var data: Data?
    var error: Error?
}

This type can represent contradictory states.

It could be loading while also containing completed data and an error.

An enum can model the valid states more precisely.

enum DownloadState {
    case idle
    case loading
    case loaded(Data)
    case failed(Error)
}

Now each value represents one valid state.

The type system helps prevent contradictory combinations.

Type Safety Is Also a Design Tool

Type safety is not limited to catching mistakes after code has been written.

It can guide the architecture.

Good types communicate:

  • Which values may be absent.
  • Which states are possible.
  • Which operations can fail.
  • Which values may cross concurrency boundaries.
  • Which capabilities a dependency provides.
  • Which object owns mutable state.

The more accurately the types describe the problem, the more invalid code the compiler can reject.

A More Senior Mental Model

A beginner may explain type safety like this:

“Swift does not let you assign a String to an Int.”

That is correct, but incomplete.

A more experienced explanation is:

“Swift is statically typed and compiled. The compiler verifies relationships between values, function signatures, protocol requirements, generic constraints, optional states, isolation domains and ownership rules before execution. The goal is to prevent invalid operations from becoming runtime failures, while still allowing explicit unsafe or forced operations when the programmer accepts responsibility.”

Common Misunderstandings

Type Safe Means the Application Cannot Crash

No.

Runtime data, forced operations and logic errors can still cause failure.

Type Inference Means Swift Is Dynamically Typed

No.

The compiler infers a static type.

A Heap Object Cannot Be Type Checked Until Runtime

No.

The runtime address and lifetime may be dynamic, while the reference’s allowed type remains known at compile time.

Objective-C Had No Type Safety

No.

Objective-C had static type information, but its dynamic messaging model allowed more operations to be resolved at runtime.

Swift Is Always Faster Than C++

No.

Performance depends upon the code, compiler, algorithms and workload.

Swift is designed for high-performance native execution while offering higher-level language features.

Using Any Makes Swift Untyped

No.

Any is itself a type representing a value whose concrete type is not yet known through that interface.

Interview Questions

What is type safety?

Type safety is the enforcement of rules that ensure values are used only in ways compatible with their types.

Is Swift statically or dynamically typed?

Swift is primarily statically typed.

Types are known and checked during compilation, although the language also supports selected runtime dynamic behaviour.

What is type inference?

Type inference is the compiler’s ability to determine a value’s static type from its expression without requiring an explicit annotation.

Why is Swift safer than a highly dynamic messaging system?

Swift attempts to verify method availability, protocol conformance and value compatibility before execution rather than deferring all checks until runtime.

Did Objective-C have a type system?

Yes.

However, its dynamic runtime allowed some message and type-related failures to occur only while the program was running.

How do optionals improve type safety?

Optionals encode the possibility of absence into the type and require the caller to handle or explicitly override that possibility.

How do generics preserve type safety?

Generics allow reusable code to express relationships between types without reducing every value to an untyped or loosely typed representation.

How do protocols support type safety?

Protocols declare required capabilities, and the compiler verifies that conforming types provide them.

What is definite initialisation?

Definite initialisation is Swift’s rule that all required stored properties must contain valid values before an instance becomes available for use.

What does exhaustive switching provide?

It ensures that every possible enum case is handled.

What safety does actor isolation provide?

Actor isolation restricts access to mutable actor state and helps prevent unsynchronised concurrent access.

What does Sendable communicate?

It communicates that a value can safely cross concurrency boundaries.

Can Swift still fail at runtime because of a type assertion?

Yes.

Forced casts, force unwraps and invalid unowned references can trap when their runtime assumptions are wrong.

Is a value on the heap less type safe than a value on the stack?

No.

Memory location and type safety are different concepts.

What is a programming paradigm?

A programming paradigm is a style or model for organising and expressing code.

Why is Swift called multi-paradigm?

Swift supports imperative, object-oriented, protocol-oriented, functional, event-driven, reactive and declarative programming styles.

Is Swift as fast as C++?

Swift is designed for high-performance native code and can be competitive with C++ in suitable workloads, but performance depends upon the implementation and should be measured rather than assumed.

Final Revision Note

Swift is a compiled, statically typed language.

Its compiler analyses the relationships between values before the program runs.

Type safety ensures that incompatible values are not silently treated as though they were the same type.

Function parameters, return values, properties, collections, protocols, generics, optionals and patterns all participate in this system.

Swift also extends compile-time safety into areas such as initialisation, access control, actor isolation, Sendable checking and ownership.

This does not mean that runtime failure is impossible.

It means that many predictable failures are rejected before an executable application is produced.

Remember

Type safety is not merely knowing that an Int is different from a String.

It is the compiler verifying the contracts and relationships formed by the types throughout the program.

Every useful compile-time error is a potential runtime bug that never reached the user.

bottom of page