Insight into Programming
Typescript-vs-CSharp
Inheritence & Polymorphism

Let’s dive into a detailed comparison of inheritance and polymorphism in TypeScript and C#, focusing on their mechanisms for class inheritance, interface implementation, method overriding, and polymorphic behavior. I’ll explain how TypeScript supports single inheritance and multiple interface implementation, relying on structural typing for polymorphism without virtual/override keywords, and how C# supports single inheritance with explicit virtual/override keywords, multiple interface implementation, and nominal typing for polymorphism. Each point will be illustrated with clear, concise examples in a pointwise manner to highlight the differences.


TypeScript: Inheritance and Polymorphism

TypeScript, as a superset of JavaScript, supports single inheritance for classes, meaning a class can extend only one base class, and multiple interface implementation, allowing a class to conform to multiple interface contracts. TypeScript uses structural typing (duck typing) for polymorphism, meaning compatibility is based on the shape of objects rather than explicit type relationships. There are no virtual or override keywords, as method overriding is handled implicitly, aligning with JavaScript’s prototype-based inheritance.

1. Single Inheritance

TypeScript classes can extend a single base class using the extends keyword, inheriting its properties and methods.

  • Example:
    class Animal {
        name: string;
     
        constructor(name: string) {
            this.name = name;
        }
     
        move(): string {
            return `${this.name} is moving`;
        }
    }
     
    class Dog extends Animal {
        bark(): string {
            return `${this.name} says Woof!`;
        }
    }
     
    const dog = new Dog("Buddy");
    console.log(dog.move()); // Output: Buddy is moving
    console.log(dog.bark()); // Output: Buddy says Woof!
    • How it Works: Dog inherits name and move from Animal. TypeScript ensures the constructor calls the base class constructor (implicitly or via super if needed).

2. Multiple Interface Implementation

A TypeScript class can implement multiple interfaces using the implements keyword, ensuring it provides all required members. Interfaces define shapes, and TypeScript’s structural typing allows flexibility.

  • Example:
    interface Printable {
        print(): string;
    }
     
    interface Loggable {
        log(): string;
    }
     
    class Document implements Printable, Loggable {
        content: string;
     
        constructor(content: string) {
            this.content = content;
        }
     
        print(): string {
            return `Printing: ${this.content}`;
        }
     
        log(): string {
            return `Logging: ${this.content}`;
        }
    }
     
    const doc = new Document("Report");
    console.log(doc.print()); // Output: Printing: Report
    console.log(doc.log()); // Output: Logging: Report
    • How it Works: Document must implement both print and log to satisfy Printable and Loggable. TypeScript checks this at compile-time.

3. No virtual/override Keywords

TypeScript does not use virtual or override keywords for method overriding. If a subclass defines a method with the same name as a base class method, it implicitly overrides it. This relies on JavaScript’s prototype chain.

  • Example:
    class Vehicle {
        move(): string {
            return "Vehicle is moving";
        }
    }
     
    class Car extends Vehicle {
        move(): string {
            return "Car is driving";
        }
    }
     
    const vehicle: Vehicle = new Car();
    console.log(vehicle.move()); // Output: Car is driving
    • How it Works: Car overrides move without explicit keywords. The actual method called depends on the object’s runtime type, demonstrating polymorphism via the prototype chain.

4. Structural Typing for Polymorphism

TypeScript uses structural typing, meaning polymorphism is based on the shape of objects (properties and methods) rather than their explicit type hierarchy. An object is compatible with a type or interface if it has the required members, regardless of its declared type.

  • Example:
    interface Shape {
        area(): number;
    }
     
    class Circle {
        radius: number;
     
        constructor(radius: number) {
            this.radius = radius;
        }
     
        area(): number {
            return Math.PI * this.radius ** 2;
        }
    }
     
    class Rectangle {
        width: number;
        height: number;
     
        constructor(width: number, height: number) {
            this.width = width;
            this.height = height;
        }
     
        area(): number {
            return this.width * this.height;
        }
    }
     
    function printArea(shape: Shape): void {
        console.log(`Area: ${shape.area()}`);
    }
     
    const circle = new Circle(5);
    const rectangle = new Rectangle(4, 6);
    printArea(circle); // Output: Area: ~78.53981633974483
    printArea(rectangle); // Output: Area: 24
    • How it Works: Circle and Rectangle are compatible with Shape because they have an area method, even though they don’t explicitly implement Shape. This structural compatibility enables polymorphism.

5. Implicit Interface Implementation

TypeScript allows classes to implicitly satisfy interfaces based on their structure, without using implements.

  • Example:
    interface User {
        name: string;
        greet(): string;
    }
     
    class Person {
        name: string;
     
        constructor(name: string) {
            this.name = name;
        }
     
        greet(): string {
            return `Hello, ${this.name}!`;
        }
    }
     
    const person: User = new Person("Alice");
    console.log(person.greet()); // Output: Hello, Alice!
    • How it Works: Person satisfies User structurally, so it can be assigned to a User variable without explicitly implementing the interface.

C#: Inheritance and Polymorphism

C# supports single inheritance for classes, meaning a class can inherit from only one base class, and multiple interface implementation, allowing a class to implement multiple interfaces. C# uses nominal typing, meaning type compatibility is based on explicit type declarations (e.g., class or interface inheritance). Polymorphism is achieved through explicit method overriding using the virtual and override keywords, ensuring precise control over which methods can be overridden.

1. Single Inheritance

C# classes can inherit from a single base class using the : operator, inheriting its members. Constructors must call the base class constructor using : base().

  • Example:
    public class Animal
    {
        public string Name { get; set; }
     
        public Animal(string name)
        {
            Name = name;
        }
     
        public string Move()
        {
            return $"{Name} is moving";
        }
    }
     
    public class Dog : Animal
    {
        public Dog(string name) : base(name) {}
     
        public string Bark()
        {
            return $"{Name} says Woof!";
        }
    }
     
    class Program
    {
        static void Main()
        {
            Dog dog = new Dog("Buddy");
            Console.WriteLine(dog.Move()); // Output: Buddy is moving
            Console.WriteLine(dog.Bark()); // Output: Buddy says Woof!
        }
    }
    • How it Works: Dog inherits Name and Move from Animal, and the constructor calls the base class constructor with : base(name).

2. Multiple Interface Implementation

C# classes can implement multiple interfaces using the : operator, requiring explicit implementation of all interface members.

  • Example:
    public interface IPrintable
    {
        string Print();
    }
     
    public interface ILoggable
    {
        string Log();
    }
     
    public class Document : IPrintable, ILoggable
    {
        public string Content { get; set; }
     
        public Document(string content)
        {
            Content = content;
        }
     
        public string Print()
        {
            return $"Printing: {Content}";
        }
     
        public string Log()
        {
            return $"Logging: {Content}";
        }
    }
     
    class Program
    {
        static void Main()
        {
            Document doc = new Document("Report");
            Console.WriteLine(doc.Print()); // Output: Printing: Report
            Console.WriteLine(doc.Log()); // Output: Logging: Report
        }
    }
    • How it Works: Document explicitly implements Print and Log, satisfying both interfaces. C# enforces this at compile-time.

3. Explicit virtual/override for Method Overriding

C# requires the virtual keyword in the base class to mark a method as overridable, and the override keyword in the derived class to override it. This ensures explicit intent and prevents accidental overrides.

  • Example:
    public class Vehicle
    {
        public virtual string Move() // Mark as overridable
        {
            return "Vehicle is moving";
        }
    }
     
    public class Car : Vehicle
    {
        public override string Move() // Explicit override
        {
            return "Car is driving";
        }
    }
     
    class Program
    {
        static void Main()
        {
            Vehicle vehicle = new Car();
            Console.WriteLine(vehicle.Move()); // Output: Car is driving
        }
    }
    • How it Works: The virtual keyword allows Move to be overridden, and override ensures Car’s implementation is used, even when referenced as a Vehicle. This is runtime polymorphism via the vtable.

4. Nominal Typing for Polymorphism

C# uses nominal typing, meaning polymorphism is based on explicit type relationships (inheritance or interface implementation). Types must be explicitly declared as related for polymorphic behavior.

  • Example:
    public interface IShape
    {
        double Area();
    }
     
    public class Circle : IShape
    {
        public double Radius { get; }
     
        public Circle(double radius)
        {
            Radius = radius;
        }
     
        public double Area()
        {
            return Math.PI * Radius * Radius;
        }
    }
     
    public class Rectangle : IShape
    {
        public double Width { get; }
        public double Height { get; }
     
        public Rectangle(double width, double height)
        {
            Width = width;
            Height = height;
        }
     
        public double Area()
        {
            return Width * Height;
        }
    }
     
    class Program
    {
        static void PrintArea(IShape shape)
        {
            Console.WriteLine($"Area: {shape.Area()}");
        }
     
        static void Main()
        {
            IShape circle = new Circle(5);
            IShape rectangle = new Rectangle(4, 6);
            PrintArea(circle); // Output: Area: ~78.53981633974483
            PrintArea(rectangle); // Output: Area: 24
        }
    }
    • How it Works: Circle and Rectangle implement IShape explicitly, enabling polymorphic behavior when passed to PrintArea. C# requires the IShape interface to establish type compatibility.

5. Explicit Interface Implementation

C# allows explicit interface implementation to resolve conflicts or hide interface members from the public API, unlike TypeScript’s implicit structural implementation.

  • Example:
    public interface IPrintable
    {
        string Print();
    }
     
    public class Document : IPrintable
    {
        string IPrintable.Print() // Explicit implementation
        {
            return "Explicitly printing";
        }
     
        public string Print() // Public method
        {
            return "Public printing";
        }
    }
     
    class Program
    {
        static void Main()
        {
            Document doc = new Document();
            Console.WriteLine(doc.Print()); // Output: Public printing
     
            IPrintable printable = doc;
            Console.WriteLine(printable.Print()); // Output: Explicitly printing
        }
    }
    • How it Works: The explicit implementation (IPrintable.Print) is only accessible when Document is cast to IPrintable, allowing separate behavior for the interface.

Key Differences Summarized with Examples

  1. Inheritance Model:

    • TypeScript: Supports single inheritance for classes, with no multiple class inheritance.
      class Animal { move() { return "Moving"; } }
      class Dog extends Animal { bark() { return "Woof"; } }
    • C#: Supports single inheritance for classes, with explicit constructor chaining.
      public class Animal { public Animal() {} public string Move() => "Moving"; }
      public class Dog : Animal { public string Bark() => "Woof"; }
  2. Interface Implementation:

    • TypeScript: Supports multiple interface implementation, with implicit structural satisfaction.
      interface A { a(): string; }
      interface B { b(): string; }
      class C implements A, B { a() { return "A"; } b() { return "B"; } }
    • C#: Supports multiple interface implementation, requiring explicit member implementation.
      public interface IA { string A(); }
      public interface IB { string B(); }
      public class C : IA, IB { public string A() => "A"; public string B() => "B"; }
  3. Method Overriding:

    • TypeScript: No virtual/override; methods are overridden implicitly by name.
      class Base { go() { return "Base"; } }
      class Derived extends Base { go() { return "Derived"; } }
      console.log(new Derived().go()); // Output: Derived
    • C#: Requires virtual in base class and override in derived class for overriding.
      public class Base { public virtual string Go() => "Base"; }
      public class Derived : Base { public override string Go() => "Derived"; }
      Console.WriteLine(new Derived().Go()); // Output: Derived
  4. Typing System for Polymorphism:

    • TypeScript: Uses structural typing, allowing polymorphism based on object shape.
      interface Shape { area(): number; }
      class Square { area() { return 16; } }
      const shape: Shape = new Square();
      console.log(shape.area()); // Output: 16
    • C#: Uses nominal typing, requiring explicit inheritance or interface implementation.
      public interface IShape { double Area(); }
      public class Square : IShape { public double Area() => 16; }
      IShape shape = new Square();
      Console.WriteLine(shape.Area()); // Output: 16
  5. Polymorphic Behavior:

    • TypeScript: Achieved via prototype chain and structural compatibility.
      class Base { act() { return "Base"; } }
      class Derived extends Base { act() { return "Derived"; } }
      const obj: Base = new Derived();
      console.log(obj.act()); // Output: Derived
    • C#: Achieved via vtable and explicit type relationships.
      public class Base { public virtual string Act() => "Base"; }
      public class Derived : Base { public override string Act() => "Derived"; }
      Base obj = new Derived();
      Console.WriteLine(obj.Act()); // Output: Derived
  6. Interface Implementation Flexibility:

    • TypeScript: Allows implicit interface satisfaction via structural typing.
      interface User { name: string; }
      const user: User = { name: "Bob" }; // No class needed
    • C#: Requires explicit interface implementation, with optional explicit syntax.
      public interface IUser { string Name { get; } }
      public class User : IUser { public string Name => "Bob"; }

FeatureTypeScriptC#
Class InheritanceSingle inheritance using extends.Single inheritance using :.
Exampleclass Dog extends Animal {}public class Dog : Animal {}
Interface ImplementationSupports multiple interface implementation using implements.Supports multiple interface implementation using :.
Exampleclass Person implements CanSpeak, CanRun {}public class Person : ICanSpeak, ICanRun {}
Method OverridingImplicit overriding; no virtual/override keywords required.Explicit with virtual in base class and override in derived class.
Exampleclass Car extends Vehicle { start() { return "Car started"; } }public class Car : Vehicle { public override string Start() { return "Car started"; } }
Typing SystemStructural typing: polymorphism based on compatible shapes (duck typing).Nominal typing: polymorphism based on explicit inheritance/interfaces.
Examplefunction printItem(item: Printable) {} works with any object having print.void PrintItem(IPrintable item) {} requires explicit IPrintable implementation.
Polymorphism EnforcementCompile-time only; erased in JavaScript output.Compile-time and runtime via .NET CLR.
FlexibilityMore flexible due to structural typing, but less strict.Stricter due to nominal typing and explicit keywords.

Summary

  • TypeScript Inheritance and Polymorphism: Supports single inheritance for classes and multiple interface implementation, relying on structural typing for polymorphism. Method overriding is implicit, without virtual/override keywords, leveraging JavaScript’s prototype chain. This makes TypeScript flexible for dynamic, web-based applications where shape-based compatibility is prioritized.
  • C# Inheritance and Polymorphism: Supports single inheritance with explicit virtual/override for method overriding and multiple interface implementation, using nominal typing for polymorphism. Explicit type relationships ensure precise control, enforced at compile-time and runtime, making C# suitable for enterprise applications requiring strict type safety.

These differences reflect TypeScript’s alignment with JavaScript’s dynamic, structural nature, versus C#’s focus on explicit, nominal type relationships in the .NET ecosystem, each tailored to their respective use cases and runtime environments.