A list of awesome value types in Swift, for a more type safe code
For motivation check and some explanation on why this might be useful, check: Bringing runtime errors to compile time (in Swift with Types)
This repo contains some cool types. Some of them are a specialization from the generic type ValidatedString.
Let's assume your model contains an User with a username and a SSN:
struct User { let username: String let ssn: String }That's not type safe, so let's do better with ValidatedString<Validator>
With very little code you create very safe data types with a lot of cool features
// First, let's create a username validator typealias Username = ValidatedString<UsernameValidator> enum UsernameValidator: StringValidator { /// Accepts any username with length between 3 and 10 inclusive static func validate(_ string: String) -> String? { let username = string.trimmingCharacters(in: .whitespacesAndNewlines) guard (3...10).contains(username.count) else {return nil} return username } } // and a SSN validator typealias SSN = ValidatedString<SSNValidator> enum SSNValidator: StringValidator { /// Accepts ssn with or without the dash, but it always stores without the dashes static func validate(_ string: String) -> String? { let string = string.trimmingCharacters(in: .whitespacesAndNewlines) let regex = #"\d{3}-?\d{2}-?\d{4}"# guard NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: string) else {return nil} return string.replacingOccurrences(of: "-", with: "") } }And change our model to use those new types
struct User { let username: Username let ssn: SSN }let usernames: [Username] = [ "Gonzula", "jgcmarins" ] let ssn: SSN = "078-05-1120" let user = User(username: "gonzula", ssn: "078051120")let userInput = "Gonzula" let optionalUsername1 = Username(userInput) // Optional("Gonzula") let invalidUserInput = "InvalidUserInputBucauseItsVeryBig" let optionalUsername2 = Username(invalidUserInput) // nillet username: Username = "Gonzula" let convertedToString = String(username) // Gonzula print("The username is \(username)") // The username is Gonzulaextension User: Codable {} let json = """ { "username": "Gonzula", "ssn": "078051120" } """.data(using: .utf8)! let decoder = JSONDecoder() let decodedUser = try! decoder.decode(User.self, from: json) let encoder = JSONEncoder() let encodedJson = try! encoder.encode(decodedUser) String(data: encodedJson, encoding: .utf8)! // {"ssn":"078051120","username":"Gonzula"}Useful for case insensitive comparison, but still preserving input's original case
extension UsernameValidator: StringNormalizer, StringComparator{ static func normalize(_ rawValue: String) -> String { return rawValue.lowercased() } static func areInIncreasingOrder(_ lhs: String, _ rhs: String) -> Bool { return normalize(lhs).localizedCaseInsensitiveCompare(normalize(rhs)) == .orderedAscending } } let scores: [Username: Int] = ["Foo": 10, "Bar": 5] scores["FOO"] // 10 scores["bar"] // 5 let players: [Username] = ["Foo", "bar"].sorted() // [bar, Foo] // while a simple string sorting will result in a different order let stringPlayers: [String] = ["Foo", "bar"].sorted() // [Foo, bar]extension SSN { var formatted: String { let digits = Array(rawValue) // Don't have to worry about out of range index because the data was already validated let groups = [digits[0..<3], digits[3..<5], digits[5..<9]] let formatted = groups.joined(separator: "-") return String(formatted) } } let ssnNumber = "078051120" SSN(ssnNumber)?.formatted // 078-05-1120