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
inheritsname
andmove
fromAnimal
. TypeScript ensures the constructor calls the base class constructor (implicitly or viasuper
if needed).
- How it Works:
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 bothprint
andlog
to satisfyPrintable
andLoggable
. TypeScript checks this at compile-time.
- How it Works:
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
overridesmove
without explicit keywords. The actual method called depends on the object’s runtime type, demonstrating polymorphism via the prototype chain.
- How it Works:
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
andRectangle
are compatible withShape
because they have anarea
method, even though they don’t explicitly implementShape
. This structural compatibility enables polymorphism.
- How it Works:
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
satisfiesUser
structurally, so it can be assigned to aUser
variable without explicitly implementing the interface.
- How it Works:
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
inheritsName
andMove
fromAnimal
, and the constructor calls the base class constructor with: base(name)
.
- How it Works:
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 implementsPrint
andLog
, satisfying both interfaces. C# enforces this at compile-time.
- How it Works:
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 allowsMove
to be overridden, andoverride
ensuresCar
’s implementation is used, even when referenced as aVehicle
. This is runtime polymorphism via the vtable.
- How it Works: The
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
andRectangle
implementIShape
explicitly, enabling polymorphic behavior when passed toPrintArea
. C# requires theIShape
interface to establish type compatibility.
- How it Works:
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 whenDocument
is cast toIPrintable
, allowing separate behavior for the interface.
- How it Works: The explicit implementation
Key Differences Summarized with Examples
-
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"; }
- TypeScript: Supports single inheritance for classes, with no multiple class inheritance.
-
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"; }
- TypeScript: Supports multiple interface implementation, with implicit structural satisfaction.
-
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 andoverride
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
- TypeScript: No
-
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
- TypeScript: Uses structural typing, allowing polymorphism based on object shape.
-
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
- TypeScript: Achieved via prototype chain and structural compatibility.
-
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"; }
- TypeScript: Allows implicit interface satisfaction via structural typing.
Feature | TypeScript | C# |
---|---|---|
Class Inheritance | Single inheritance using extends . | Single inheritance using : . |
Example | class Dog extends Animal {} | public class Dog : Animal {} |
Interface Implementation | Supports multiple interface implementation using implements . | Supports multiple interface implementation using : . |
Example | class Person implements CanSpeak, CanRun {} | public class Person : ICanSpeak, ICanRun {} |
Method Overriding | Implicit overriding; no virtual /override keywords required. | Explicit with virtual in base class and override in derived class. |
Example | class Car extends Vehicle { start() { return "Car started"; } } | public class Car : Vehicle { public override string Start() { return "Car started"; } } |
Typing System | Structural typing: polymorphism based on compatible shapes (duck typing). | Nominal typing: polymorphism based on explicit inheritance/interfaces. |
Example | function printItem(item: Printable) {} works with any object having print . | void PrintItem(IPrintable item) {} requires explicit IPrintable implementation. |
Polymorphism Enforcement | Compile-time only; erased in JavaScript output. | Compile-time and runtime via .NET CLR. |
Flexibility | More 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.