Skip to main content

Command Palette

Search for a command to run...

Type Safety in Swift: Haskell-style Newtypes with Phantom Types

Published
4 min read

In software engineering, "primitive obsession" describes a scenario where domain-specific concepts are represented by general types like strings, integers, or timestamps. This lack of specificity often leads to logic errors, for instance, accidentally passing an arrival timestamp into a function that expects a departure timestamp.

Haskell addresses this through the newtype construct, which creates a distinct type from an existing one with zero runtime cost. In Swift, we can replicate this rigorous type safety by combining Phantom Types with the RawRepresentable protocol.

The Compile-Time Safety Guarantee

The primary advantage of this pattern is that it shifts domain logic errors from runtime to compile time. If we define distinct types for our arrival and departure timestamps, the Swift compiler physically prevents us from swapping them.

Swift

func createFlight(destination: String, arrival: NewType<ArrivalTag>, departure: NewType<DepartureTag>) -> Flight {
    // ❌ Compiler Error: Cannot convert value of type 'NewType<ArrivalTag>' to expected argument type 'NewType<DepartureTag>'
    return Flight(arrival: departure, departure: arrival, destination: destination)
}

By explicitly tagging the types, the mistake of passing departure into the arrival parameter is caught immediately during development.

The Implementation

The following code demonstrates the generic NewType wrapper. By using empty enums as "Tags", we create distinct types for arrival and departure that the compiler will treat as strictly incompatible.

Swift

import Foundation

struct NewType<Tag>: RawRepresentable, Codable {
    let rawValue: TimeInterval
}

extension NewType: Hashable  {}
extension NewType: Equatable {}

enum ArrivalTag {}
enum DepartureTag {}

struct Flight: Hashable, Codable {
    let arrival: NewType<ArrivalTag>
    let departure: NewType<DepartureTag>
    let destination: String
}

let flightJSON = """
{
    "destination": "London LHR",
    "arrival": 2345,
    "departure": 876867
}
"""

func decodeFlight(from jsonString: String) -> Flight? {
    let decoder = JSONDecoder()
    
    guard let data = jsonString.data(using: .utf8) else { return nil }

    do {
        return try decoder.decode(Flight.self, from: data)
    } catch {
        print("❌ Decoding failed: \(error)")
        return nil
    }
}

if let flight = decodeFlight(from: flightJSON) {
    print("✅ Successfully decoded Flight:")
    print("   Destination: \(flight.destination)")
    print("   Arrival    : \(flight.arrival.rawValue)")
    print("   Departure  : \(flight.departure.rawValue)")
}

Functional Programming and Category Theory Concepts

Phantom Types

The Tag parameter in the NewType struct is a Phantom Type. It exists only in the type signature and does not appear in the stored properties of the struct. Its sole function is to serve as a marker for the compiler. This ensures that NewType<ArrivalTag> and NewType<DepartureTag> are seen as entirely different entities.

Isomorphism

From the perspective of category theory, the NewType struct is isomorphic to the underlying TimeInterval. This means there is a total, one-to-one mapping between a TimeInterval and a NewType<Tag> instance.

By utilising RawRepresentable, we preserve this isomorphism. We gain the semantic clarity of a custom type without losing the ability to access and manipulate the data in its original primitive form.

The Advantage of RawRepresentable and Flat Data

In Swift, defining a standard wrapper struct often results in nested JSON structures. For example, without specific conformance, a NewType might encode to a nested object like {"arrival": {"rawValue": 2345}}.

By adopting RawRepresentable, the Swift Codable system recognises the struct as a transparent wrapper around its rawValue. This enables flat data encoding: the JSON remains a clean, standard numeric timestamp, while the Swift domain model remains strictly typed. This provides the exact safety of Haskell's newtype while maintaining perfect compatibility with existing, flat API structures.

Conclusion

Utilising this pattern offers several critical benefits for architecture:

  • Strict Type Safety: Logic errors regarding parameter roles are caught at compile time.

  • No Performance Overhead: Structs are value types that the Swift compiler can heavily optimise, treating them identically to the underlying primitive at runtime.

  • Declarative Intent: The code explicitly documents the role of each piece of data through the type system rather than relying on variable naming conventions.