Type Safety Across the Stack: From TypeScript to Go

Type Safety Across the Stack: From TypeScript to Go

For the past five years, my work as a software developer has centered on JavaScript, primarily React, Next.js, and Nodejs. This experience has been complemented by some backend development using Spring Boot as well. Recently, I've realized that I should focus more on core engineering principles rather than relying too heavily on specific frameworks. This realization led me to explore Go, emphasizing using its standard library for project development. The transition was exciting given my previous enthusiasm for TypeScript and its ability to enhance code reliability through its type system.

This article shares what I learned about the importance of type safety across the entire application stack and its role in reducing potential bugs. By comparing TypeScript and Go, we'll explore how they handle type safety and the impact this has on development practices and code quality.

So, what is Type Safety and why is it important? Type safety prevents operations on incompatible data types, such as performing string operations on numbers or adding a number to a boolean - issues common in loosely typed languages like JavaScript. It ensures that values in a program are used consistently and as intended. The benefits of type safety include:

  • Catching errors at compile-time, reducing runtime crashes

  • Enhancing code readability through type annotations that serve as documentation

  • Improving IDE support for automated refactoring

  • Enabling more effective code optimization by compilers

  • Encouraging clearer API designs with explicit function signatures

  • Saving time in debugging and maintenance, despite initial setup costs

After experiencing the benefits of TypeScript, I finally feel like JavaScript is being fixed and is on the path to stability. It's clear that JavaScript frameworks will continue to emerge, introducing new problems along the way. However, with TypeScript at the core of many frameworks, we gain a deeper understanding of their inner workings. Type annotations for methods and handlers provide valuable insights, making it easier for junior developers to grasp the underlying logic and potential use cases.

If you haven't used TypeScript, the following code block contains a set of fundamental features of TypeScript put together.

// Basic Types
let num: number = 10;
let str: string = "Hello, TypeScript!";
let bool: boolean = true;
// Arrays
let numbers: number[] = [1, 2, 3];
let strings: string[] = ["a", "b", "c"];
// Null and Undefined
let u: undefined = undefined;
let n: null = null;
// Tuple
let person: [string, number] = ["John Doe", 30];
// Enum
enum Color {
    Red,
    Green,
    Blue
}
let myColor: Color = Color.Green;
// Interfaces
interface Person {
    firstName: string;
    lastName: string;
    age: number;
}
let person1: Person = {
    firstName: "Jane",
    lastName: "Doe",
    age: 25
};
// Classes
class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
class Dog extends Animal {
    bark() {
        console.log("Woof!");
    }
}
let dog = new Dog("Snoopy");
dog.bark();
dog.move(10);
// Function Parameters
function buildName(firstName: string,, middleName?: string, lastName = "Smith", ...restOfName: string[])) {
    return firstName + middleName?middleName:" " + lastName + restOfName.join(" ");
}

// Generics
function identity<T>(arg: T): T {
    return arg;
}

// Modules
// (example using ES6 modules)
export interface SquareConfig {
    color?: string;
    width?: number;
}

export function createSquare(config: SquareConfig): { color: string; area: number } {
    let newSquare = { color: "white", area: 100 };
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

After a few projects like implementing my own git using typescript (Github repo), I was ready to take my typescript skills to React and Next.js. However, the challenges of accurately typing external library methods, often circumvented by the 'any' keyword, became a persistent hurdle. This led to a temporary retreat to JavaScript, where the absence of TypeScript's safety net was noticeable. I made a solid decision that I would go back to typescript and avoid the "any" keyword. After a bit of struggle, I regained my coding momentum. As I progressed, I found that even the most basic tasks required thoughtful consideration of data structures and logic, akin to solving intricate puzzles. This engagement, coupled with the satisfaction of crafting robust code, inspired a shift in focus towards lower-level systems programming and the Go language.

Like most other developers, I started with the gobyexample website. The transition from typescript to go was really easy and I got the same coding stimulations from typescript. It was exciting to discover I could accomplish much more without the limitations often encountered in JavaScript. The standard library was so powerful that you didn't need to rely on any external packages for small projects. Go is a statically typed language that enforces string type checking, unlike typescript. The benefits of type safety apply to Go as well. I thought to myself that I could now build new stuff while enjoying coding, without having to learn new abstractions on top of core engineering principles.

The following code snippet shows some examples of the type system in Go:

package main

import "fmt"

func main() {
        // Basic types
        var age int = 30
        var name string = "Alice"
        var isStudent bool = true
        var price float64 = 19.99

        // Type inference
        greeting := "Hello, world!"

        // Arrays
        var numbers [3]int = [3]int{1, 2, 3}
        var letters [5]string

        // Slices
        slice1 := numbers[1:3]
        slice2 := make([]int, 5)

        // Structs
        type Person struct {
                Name string
                Age  int
        }
        person1 := Person{"Bob", 25}

        // Maps
        ages := make(map[string]int)
        ages["Alice"] = 30
        ages["Bob"] = 25

        // Functions
        func add(x, y int) int {
                return x + y
        }
        sum := add(2, 3)

        // Pointers
        var x int = 10
        var p *int = &x
        *p = 20

        // Interfaces
        type Shape interface {
                area() float64
        }
        type Rectangle struct {
                width, height float64
        }
        func (r Rectangle) area() float64 {
                return r.width * r.height
        }
        var shape Shape = Rectangle{5, 3}
        fmt.Println(shape.area())
}

Now let me dive into the comparison:

Go and TypeScript both employ static type systems, meaning that type checking occurs at compile time rather than runtime. This significantly enhances code reliability and maintainability. However, there are some key differences in how they approach type safety.

Similarities

  • Static Typing: Both languages enforce type safety at compile time.

  • Basic Types: Both have similar basic types like integers, floating-point numbers, booleans, and strings.

  • Composite Types: Both support arrays, structs (or interfaces in TypeScript), and maps.

  • Functions: Both have function types with parameters and return values.

Differences

  • Struct vs Interface: Go primarily uses structs for data modeling, while TypeScript leans towards interfaces.

  • Generics: Go introduced generics relatively recently (1.18), while TypeScript has had them for longer.

  • Type Inference: Go has more aggressive type inference than TypeScript.

  • Type Assertions: Go uses type assertions, while TypeScript has type guards and the as operator.

Both Go and TypeScript prioritize code reliability through their strong type systems. While they share many fundamental concepts, Go's simpler syntax and stricter typing rules create a sense of confidence in code correctness. The clarity and directness of Go's approach, as highlighted in "Effective Go," make it a compelling choice for building robust applications.

My experience with both languages has reinforced the value of static typing for developing reliable software. I'm increasingly drawn to using type-safe languages like Go and TypeScript for future projects, as they offer a solid foundation for creating high-quality products. I encourage developers working with dynamically typed languages to consider the benefits of adopting static typing to enhance code quality and maintainability.