DEV Community

ANKUSH CHOUDHARY JOHAL
ANKUSH CHOUDHARY JOHAL

Posted on • Originally published at johal.in

How to Implement Traits in Rust 1.86 with TypeScript 5.7 and Go 1.24 for Polymorphism

How to Implement Traits for Polymorphism in Rust 1.86, TypeScript 5.7, and Go 1.24

Polymorphism is a core object-oriented principle that allows objects of different types to be treated as objects of a common type. Traits (or interfaces, protocols) are the primary mechanism to achieve this in many modern languages. This guide walks through implementing trait-based polymorphism in Rust 1.86, TypeScript 5.7, and Go 1.24, with practical code examples and key differences between each approach.

Prerequisites

  • Basic knowledge of Rust, TypeScript, and Go syntax
  • Rust 1.86 installed (verify with rustc --version)
  • TypeScript 5.7 installed (verify with tsc --version)
  • Go 1.24 installed (verify with go version)

What Are Traits?

Traits define a set of method signatures that a type must implement to conform to the trait. They enable polymorphism by letting you write code that works with any type that implements the trait, without needing to know the type's concrete details. While Rust calls these "traits", TypeScript uses "interfaces" and Go uses "interfaces" as well — all serve the same core purpose for polymorphism.

Implementing Traits in Rust 1.86

Rust's trait system supports both static (compile-time) and dynamic (runtime) polymorphism. Static dispatch uses generics with trait bounds, while dynamic dispatch uses trait objects (dyn Trait).

Step 1: Define a Trait

Define a trait with the trait keyword, listing required method signatures:

trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement the Trait for Types

Use the impl Trait for Type syntax to implement the trait for concrete types:

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
    fn perimeter(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Use Traits for Polymorphism

For static dispatch (compile-time, no runtime overhead), use generics with trait bounds:

fn print_shape_info(shape: &T) {
    println!("Area: {}", shape.area());
    println!("Perimeter: {}", shape.perimeter());
}
Enter fullscreen mode Exit fullscreen mode

For dynamic dispatch (runtime, flexible type erasure), use trait objects:

fn print_dynamic_shape_info(shapes: &[&dyn Shape]) {
    for shape in shapes {
        println!("Area: {}", shape.area());
        println!("Perimeter: {}", shape.perimeter());
    }
}
Enter fullscreen mode Exit fullscreen mode

Rust 1.86 includes stabilized features like impl Trait in more positions, but the core trait system remains consistent with prior versions.

Implementing Traits in TypeScript 5.7

TypeScript uses interfaces to define trait-like contracts. Since TypeScript compiles to JavaScript (which has no native interface concept), interfaces are erased at runtime — polymorphism is enforced at compile time, with runtime checks possible via type guards.

Step 1: Define an Interface

Define an interface with the interface keyword, listing method signatures:

interface Shape {
    area(): number;
    perimeter(): number;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement the Interface

TypeScript uses structural typing (duck typing) — any type that matches the interface's shape automatically implements it, no explicit implements keyword required (though implements is recommended for clarity):

class Circle implements Shape {
    constructor(public radius: number) {}

    area(): number {
        return Math.PI * this.radius * this.radius;
    }

    perimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}

class Rectangle implements Shape {
    constructor(public width: number, public height: number) {}

    area(): number {
        return this.width * this.height;
    }

    perimeter(): number {
        return 2 * (this.width + this.height);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Use Interfaces for Polymorphism

Write functions that accept the interface type, working with any conforming object:

function printShapeInfo(shape: Shape): void {
    console.log(`Area: ${shape.area()}`);
    console.log(`Perimeter: ${shape.perimeter()}`);
}

// Works with any Shape-conforming object
const circle = new Circle(5);
const rect = new Rectangle(4, 6);
printShapeInfo(circle);
printShapeInfo(rect);
Enter fullscreen mode Exit fullscreen mode

TypeScript 5.7 adds improved type inference for interfaces and better support for implements checks on complex types.

Implementing Traits in Go 1.24

Go's interface system is implicitly implemented (structural typing, like TypeScript) — no explicit declaration of intent to implement an interface is needed. Interfaces are satisfied automatically if a type has all methods defined in the interface.

Step 1: Define an Interface

Define an interface with the type ... interface syntax, listing method signatures:

type Shape interface {
    Area() float64
    Perimeter() float64
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement the Interface

Define types with methods matching the interface — Go will automatically consider them as implementing the interface:

type Circle struct {
    Radius float64
}

type Rectangle struct {
    Width  float64
    Height float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}
Enter fullscreen mode Exit fullscreen mode

Note: You must import the math package at the top of your Go file for math.Pi.

Step 3: Use Interfaces for Polymorphism

Write functions that accept the interface type, working with any conforming type:

func printShapeInfo(s Shape) {
    fmt.Printf("Area: %v\n", s.Area())
    fmt.Printf("Perimeter: %v\n", s.Perimeter())
}

func main() {
    circle := Circle{Radius: 5}
    rect := Rectangle{Width: 4, Height: 6}
    printShapeInfo(circle)
    printShapeInfo(rect)
}
Enter fullscreen mode Exit fullscreen mode

Go 1.24 includes minor improvements to interface runtime checks and better error messages for unimplemented interface methods.

Key Differences Between Implementations

  • Rust: Traits require explicit implementation, support both static and dynamic dispatch, no runtime overhead for static dispatch.
  • TypeScript: Interfaces are structurally typed, erased at runtime, compile-time only checks (unless using type guards).
  • Go: Interfaces are structurally typed, implicitly implemented, runtime polymorphism via interface values.

Best Practices

  • Prefer static dispatch (generics) in Rust when possible for performance.
  • Use explicit implements in TypeScript to avoid accidental interface mismatches.
  • Keep Go interfaces small (single-method interfaces are common) for flexibility.
  • Test trait/interface implementations thoroughly to catch missing methods early.

Conclusion

All three languages support trait-based polymorphism, but with different tradeoffs: Rust offers fine-grained control over dispatch, TypeScript prioritizes developer ergonomics with structural typing, and Go provides simple, implicit interface implementation. Choose the approach that fits your project's needs and language constraints.

Top comments (0)