Learn Swift as a TypeScript-Experienced Developer

Learn Swift as a TypeScript-Experienced Developer

In this article, I am going to share my experience of learning Swift programming language as a developer coming from Typescript knowledge.

Display to Standard Output

console.log("Hello World!");
print("Hello World!")

Variable Declaration

const PI = 3.14; // Constants
let age = 23; // Variable, Recommended.
var age = 23; // Variable, Not Recommended.
let PI = 3.14 // Constants
var age = 23 // Variable

Declaration of constants can confuse, since let is used to create variables in Typescript and let is used to create constants in Swift.

Variable Type Declaration

The type of the variable will be inferred based on the initial value of the variable. In Typescript, constant PI and variable age will infer number type. In Swift, constant PI will infer Double and variable age will infer Int type.

In Typescript, Variables can be assigned multiple types using the union | operator can assign different types of values to it.

let customVariable: string | boolean | undefined;

In Swift, Variables can be assigned only a single type, Once the type is declared or inferred, it cannot change.

var customVariable: String?

No implicit Type Conversion

Typescript will implicity modify the type of variable based on the operator or situation.

In Typescript, console.log("10" + 1) statement will result in "101", where 1 is converted to string and is concerned with "10".

In Swift, print("10" + 1) will result in an error with the below message.

note: overloads for '+' exist with these partially matching parameter lists: (Int, Int), (String, String)

If we need the same behaviour as Typescript, then we must explicitly convert 1 to String.

The statement will be print("10" + String(1)).

String

In Typescript, we can create string variables using

  1. Single line: single quotation mark '', double quotation mark "" or back-ticks ``

  2. Multiline: back-ticks ``

In Swift, we can create string variables using

  1. Single line: double quotes ""

  2. Multiline: three double quotation mark """

    Only double quotation mark is used in Swift.

String Interpolation

In Typescript, we use ${} within string wrapped with back-ticks `` .

console.log(`The width is ${100}`);

In Swift, we use \() within string wrapped with double quotes "". Expression within \() will be evaluated to create an actual string and is replaced with that string.

print("The width is \(100)")

Arrays

The creation of Arrays is the same in both Typescript and Swift. The only difference is the way the array type is represented and unlike Typescript, Swift will throw an error when we try to access an index outside the array.

const fruits: string[] = ['Orange'];
let fruits: [String] = ["Apple"]

Dictionary

A dictionary stores associations between keys of the same type and values of the same type in a collection with no defined ordering. I think of them as arrays with custom indexes.

var responseMessages: [Int: String] = [
    200: "OK",
    403: "Access forbidden",
    404: "File not found",
    500: "Internal server error"
]
print(responseMessages[200]) // Prints "OK"

From a Typescript perspective, they are similar objects where the keys and values are limited to a single type.

const responseMessage: {[key: number]: string} = {
    200: "OK",
    403: "Access forbidden",
    404: "File not found",
    500: "Internal server error"
};
console.log(responseMessage["200"]); // Logs "OK"

Optionals

An optional represents two possibilities: Either there is a value, and you can unwrap the optional to access that value, or there isn’t a value at all.

var optionalMessage: String?
optionalMessage = "Hello!"

The question mark indicates that the value it contains is optional. When optionalMessages is defined as an optional variable without providing a default value, the variable is automatically set to nil.

In Typescript, Optionals variables can be defined by adding either null or undefined.

let optionalMessage: String | undefined;
optionalMessage = "Hello!";

Optional Checking

If Statements and Forced Unwrapping

Use an if statement to find out whether an optional contains a value by comparing the optional against nil. If an optional has a value, it’s considered to be “not equal to” nil.

if optionalMessage != nil {
    print("optionalMessage contains a message.")
}

Optional Binding

Optional binding is used to find out whether an optional contains a value, and if so, to make that value available as a temporary constant or variable.

  • If optionalMessage != nil, message is assigned with optionalMessage value and message value is printed.

  • If optionalMessage == nil, else block is executed.

if let message = optionalMessage {
    print(message)
} else {
    print("No message")
}

Nil-Coalescing Operator

The nil-coalescing operator (a ?? b) unwraps an optional a if it contains a value, or returns a default value b if a is nil. The expression a is always of an optional type. The expression b must match the type that’s stored inside a.

let message = optionalMessage ?? "No Message"

Switch

A switch statement considers a value and compares it against several possible matching patterns.

let vegetable = "red pepper"
switch vegetable {
    case "celery":
        print("Celery")
    case "cucumber", "watercress":
        print("Sandwich ingredients")
    case let x where x.hasSuffix("pepper"):
        print("Spicy \(x)")
    default:
        print("Unknown ingredient")
}

The difference between Typescript switch cases are:

  1. No Implicit Fallthrough: break statements are not required at the end of a case block.

  2. Compound Cases: The body of each case must contain at least one executable statement. Multiple switch cases that share the same body can be combined by writing several patterns after case, with a comma , between each of the patterns.

     var letter: Character = "a"
     switch(letter) {
         case "a", "e", "i", "o", "u":
             print("\(letter) is vowel")
         default:
             print("\(letter) is consanant")
    
  3. Swift case supports other types of case values like intervals, tuples, and where.

Functions

Defining and Calling Functions

func addWithNamedParameters(a: Int, b: Int) -> Int {
    return a + b
}

func addWithoutNamedParameters(_ a: Int, _ b: Int) -> Int {
    return a + b
}
  • func is the keyword to define functions

  • addWithNamedParameters is the name of the function.

  • a and b are named parameters of the function.

  • The return type of the function is the type defined after -> .

  • If an argument label for a parameter is not needed then, we can write an underscore (_) instead of an explicit argument label for that parameter.

      print(addWithNamedParameters(a: 10, b: 20)) // Prints 30
      print(addWithoutNamedParameters(10, 20)) // Prints 30
    
  • We can use a tuple type as the return type for a function to return multiple values as part of one compound return value.

      func calculate(a: Int, b: Int) -> (sum: Int, avg: Double) {
          let sum = a + b
          return (sum, Double(sum) / 2)
      }
      print(calculate(a: 10, b: 20))
    

Default Parameters

We can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

func customPrint(a: Int = 3, b: Int = 5) {
    print(a, b)
}
customPrint(a: 10, b: 20) // Prints 10 20
customPrint(b: 20) // Prints 3 2
customPrint() // Prints 3 5

Variadic Parameters

Variadic parameters are similar to Typescript's Rest parameters.

A variadic parameter accepts zero or more values of a specified type. Write variadic parameters by inserting three-period characters (...) after the parameter’s type name.

func add(_ numbers: Int...) -> Int {
    var total = 0
    for number in numbers {
        total += number
    }
    return total
}
print(add(1,2,3,4)) // Prints 10

In-Out Parameters

Function parameters are constants by default, updating them within the function body will result in a compile-time error which prevents modifying the parameter value by mistake. If we want a function to modify a parameter’s value, and we want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.

func swap(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}
var a = 3, b = 4
print(a, b) // Prints 3 4
swap(&a, &b)
print(a, b) // Prints 4 3

Closure Expressions

Closure Expressions are similar to Typescript's arrow function syntax.

Closure expressions are a way to write inline closures in a brief, focused syntax. Closure expressions provide several syntax optimizations for writing closures in a shortened form without loss of clarity or intent.

The Sorted Method

The sorted(by:) method accepts a closure that takes two arguments of the same type as the array’s contents, and returns a Bool value to say whether the first value should appear before or after the second value once the values are sorted. The sorting closure needs to return true if the first value should appear before the second value, and false otherwise.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

Implicit Returns from Single-Expression Closures

To reduce the syntax, We can omit the return type in the single expression closure, where the return type is implicity inferred.

var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in s1 > s2 })

Inferring Type From Context

Swift can infer the types of its parameters and the type of value it returns. The sorted(by:) method is being called on an array of strings, so its argument must be a function of type (String, String) -> Bool.

var reversedNames = names.sorted(by: { (s1, s2) in s1 > s2 })

Shorthand Argument Name

Swift automatically provides shorthand argument names to inline closures, which can be used to refer to the values of the closure’s arguments by the names $0, $1, $2, and so on.

var reversedNames = names.sorted(by: { $0 > $1 })

Operator Methods

Swift’s String type defines its string-specific implementation of the greater-than operator (>) as a method that has two parameters of type String, and returns a value of type Bool. This exactly matches the method type needed by the sorted(by:) method. Therefore, you can simply pass in the greater-than operator, and Swift will infer that you want to use its string-specific implementation.

var reversedNames = names.sorted(by: >)

Classes

Classes are reference types that are not copied when they’re assigned to a variable or constant, or when they’re passed to a function. Rather than a copy, a reference to the same existing instance is used.

Definition Syntax

class NamedShape {
    var numberOfSides: Int = 0
    var name: String

    init(_ name: String, numberOfSides: Int) {
        self.name = name
        self.numberOfSides = numberOfSides
    }

    func getDescription() -> String { 
        return "A \(name) has \(numberOfSides) sides."
    }
}

var shape = NamedShape("Triangle", numberOfSides: 3)
  • A class definition starts with class keyword followed by class name, which can be followed by a list of classes or protocols to support inheritance or comply with protocol structure. From the example NamedShape is the class name.

  • A class can contain a set of properties, which can have a default value. From the example numberOfSides and name are instance properties.

  • init will be called when the instance of the class is created. If a property defined for a class does not have a default value or it is an optional value. It is mandatory to set that property in the init.
    Inside init the names of the parameters and properties are the same, to differentiate the two, we use self to refer to the properties while the parameters can be used directly.

  • A class can contain a set of methods. They support the functionality of those instances, either by providing ways to access and modify instance properties or by providing functionality related to the instance’s purpose. From the example getDescription() is an instance method.

Inheritance

A class can inherit methods, properties, and other characteristics from another class. When one class inherits from another, the inheriting class is known as a subclass, and the class it inherits from is known as its superclass.

class Square: NamedShape {
    var sideLength: Double
    var perimeter: Double {
        get {
            return sideLength * 4
        }
        set {
            sideLength = newValue / 4
        }
    }

    init(sideLength: Double, name: String) {    
        super.init(name, numberOfSides: 4)
        self.sideLength = sideLength
    }

    func area() -> Double {
        return sideLength * sideLength
    }

    override func description() -> String {
        return "A \(name) with sides of length \(sideLength)"
    }
}

var square = Square(sideLength: 10.0, name: "Square")
  • Square subclass is derived from NamedShape base class.

  • Square class contains its own set of properties like sideLength, perimeter as well as the properties inherited from NamedShape class.

  • The init() initializer for Square starts by calling super.init(), which calls the default initializer for the Square class’s superclass, NamedShape. This ensures that the inherited property is initialized by NamedShape before Square has the opportunity to modify the property. Then Square class properties are initialized.

  • Square class contains its own set of methods like area() and Square class has a choice either to use the methods provided by its superclass NamedShape or to override the method to provide an alternative implementation of the method.

Computed Properties

We can provide a getter and an optional setter to retrieve and set other properties and values indirectly. For example perimeter property present in the Square definition.

shape.perimeter // 40.0

When we retrieve perimeter value, perimeter value will be computed based on the sideLength value and returned.

shape.perimeter = 80.0 // sideLength will be set as 20.0

Similarly, When we set perimeter value, the value set will be available using newValue variable by default. We can use this value to update sideLength value.

Protocols

Protocols are similar to Typescript's Interfaces. It defines a blueprint of methods, properties, and other requirements that can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements.

protocol ExampleProtocol {
    var description: String { get }
    // In class, functions are able to modify the structure by default. mutating keyword is not needed.
    // In struct, functions are not able to modify the structure by default. mutating keyword is needed.
    mutating func adjust()
}

class ExampleProtocolClass: ExampleProtocol {
    var description: String = "A simple class."
    func adjust() {
        description += " Adjusted"
    }
}

struct ExampleProtocolStructure: ExampleProtocol {
    var description: String = "A simple class."
    mutating func adjust() {
        description += " Adjusted"
    }
}

Extensions

Extensions add new functionality to an existing class, structure, enumeration, or protocol type.

extension Int: ExampleProtocol {
    var description: String { "The number is \(self)"}
    mutating func increment() {
        self += 1
    }
}
var num = 10
print(num.description) // The number is 10
num.increment()
print(num.description) // The number is 11

We are extending the Int type to have a property description which has a read-only getter and a method increment() to increment its value by 1.

Enumeration

CompassPoint enum uses multiple protocols Int and CaseIterable.

Int type is used to implicitly fill the without values with default values. It also provides access to the raw value of the cases by exposing rawValue property.

CaseIterable type is used to expose allCases which can be used to iterate over all the cases in the enum.

Unlike Typescript where an enum only supports constant values, an enum in Swift can store any type of data and also provides a feature to write methods inside them.

enum CompassPoint: Int, CaseIterable {
    case north
    case south
    case east
    case west
}

var directionToHead = CompassPoint.north

switch directionToHead {
    case .north:
        print("North")
    case .south:
        print("South")
    case .east:
        print("East")
    case .west:
        print("West")
}

for direction in CompassPoint.allCases {
    print(direction.rawValue)
}

When using switch and enum together, if all the cases are not addressed then a compile-time error will be thrown. When it isn’t appropriate to provide a case for every enumeration case, you can provide a default case to cover any cases that aren’t addressed explicitly.

Concurrency

Swift has built-in support for writing asynchronous and parallel code in a structured way.

There are no promises here.

Asynchronous code is written by defining asynchronous functions. To indicate that a function or method is asynchronous, we write the async keyword in its declaration after its parameters.

func fetchUserId(from server: String) async -> Int {
    if server == "primary" {
        return 97
    }
    return 501
}
func fetchUsername(from server: String) async -> String {
    let userId = await fetchUserId(from: server)
    if userId == 97 {
        return "John"
    }
    return "Guest"
}
// Use Task to call asynchronous functions from synchronous code, without waiting for them to return.
Task {
   let username = await fetchUsername(from: "primary")
//    print(username)
}

Calling Asynchronous Functions in Sequence

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]

In this case, await is added to every downloadPhoto() function, the code will wait and execute each downloadPhoto() function one after the other and finally stores all the results into photos variable.

Calling Asynchronous Functions in Parallel

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]

In this case, async is added to every downloadPhoto() function, the code will not wait and execute each downloadPhoto() function one after the other and finally will wait for all the results and store the results into photos variable.

References

Swift Documentation

Did you find this article valuable?

Support yogendrakumarvr by becoming a sponsor. Any amount is appreciated!