Insight into Programming
Typescript-vs-CSharp
Access Modifiers

Let’s dive into a detailed comparison of access modifiers in TypeScript and C#, focusing on how they control the visibility and accessibility of class members (fields, properties, methods, etc.), their enforcement mechanisms, and their behavior at compile-time and runtime. I’ll explain TypeScript’s public, private, and protected modifiers, which are enforced only at compile-time and erased in JavaScript, and C#’s modifiers, which are enforced at both compile-time and runtime for stricter encapsulation. Each point will be illustrated with clear, concise examples in a pointwise manner to highlight the differences.


TypeScript: Access Modifiers

TypeScript supports public, private, and protected access modifiers to control the visibility of class members. These modifiers are checked during compilation to ensure type safety, but they are erased in the generated JavaScript, meaning there’s no runtime enforcement. This allows runtime bypass of access restrictions, aligning with JavaScript’s dynamic nature.

1. Public Modifier

The public modifier allows a class member to be accessed from anywhere (inside the class, subclasses, or externally). In TypeScript, members are public by default if no modifier is specified.

  • Example:
    class Person {
        public name: string; // Explicitly public
     
        constructor(name: string) {
            this.name = name;
        }
     
        public greet(): string {
            return `Hello, ${this.name}!`;
        }
    }
     
    const person = new Person("Alice");
    console.log(person.name); // Output: Alice (accessible externally)
    console.log(person.greet()); // Output: Hello, Alice!
    • How it Works: The name property and greet method are accessible everywhere because they are public. The compiled JavaScript has no public keyword, as all properties are inherently accessible:
      class Person {
          constructor(name) {
              this.name = name;
          }
          greet() {
              return `Hello, ${this.name}!`;
          }
      }

2. Private Modifier

The private modifier restricts access to within the class. TypeScript enforces this at compile-time, but the compiled JavaScript does not prevent access, allowing runtime bypass.

  • Example:
    class Employee {
        private salary: number;
     
        constructor(salary: number) {
            this.salary = salary;
        }
     
        getSalary(): number {
            return this.salary; // OK: Accessed within class
        }
    }
     
    const emp = new Employee(50000);
    // console.log(emp.salary); // Error: Property 'salary' is private and only accessible within class 'Employee'
    console.log(emp.getSalary()); // Output: 50000
    • Runtime Bypass Example:
      const emp = new Employee(50000);
      console.log((emp as any).salary); // Output: 50000 (bypasses compile-time check)
    • Compiled JavaScript:
      class Employee {
          constructor(salary) {
              this.salary = salary;
          }
          getSalary() {
              return this.salary;
          }
      }
    • How it Works: TypeScript prevents direct access to salary at compile-time, but in JavaScript, salary is a regular property, accessible via emp.salary or type coercion (e.g., as any).

3. Protected Modifier

The protected modifier allows access within the class and its subclasses but not externally. Like private, it’s enforced only at compile-time.

  • Example:
    class Vehicle {
        protected speed: number;
     
        constructor(speed: number) {
            this.speed = speed;
        }
     
        protected getSpeed(): number {
            return this.speed;
        }
    }
     
    class Car extends Vehicle {
        describe(): string {
            return `Speed: ${this.speed}, accessed via ${this.getSpeed()}`; // OK: Protected members accessible in subclass
        }
    }
     
    const car = new Car(120);
    console.log(car.describe()); // Output: Speed: 120, accessed via 120
    // console.log(car.speed); // Error: Property 'speed' is protected and only accessible within class 'Vehicle' and its subclasses
    • Runtime Bypass Example:
      const car = new Car(120);
      console.log((car as any).speed); // Output: 120 (bypasses compile-time check)
    • How it Works: TypeScript restricts speed access outside Vehicle or its subclasses, but the compiled JavaScript allows access since speed is a regular property.

4. Compile-Time Enforcement Only

Access modifiers in TypeScript are purely a compile-time feature. After compilation, the JavaScript output contains no access control, making all members effectively public at runtime.

  • Example:
    class Test {
        private secret: string = "hidden";
    }
     
    const test = new Test();
    // console.log(test.secret); // Error at compile-time
    console.log((test as any).secret); // Output: hidden (runtime access)
    • Compiled JavaScript:
      class Test {
          constructor() {
              this.secret = "hidden";
          }
      }

5. Shorthand Constructor Syntax

TypeScript allows access modifiers in constructor parameters to automatically declare and initialize properties.

  • Example:
    class User {
        constructor(public name: string, private id: number) {}
    }
     
    const user = new User("Bob", 123);
    console.log(user.name); // Output: Bob
    // console.log(user.id); // Error: Property 'id' is private
    console.log((user as any).id); // Output: 123 (runtime bypass)

C#: Access Modifiers

C# supports a broader set of access modifiers (public, private, protected, internal, protected internal, private protected) that are enforced at both compile-time and runtime, ensuring strict encapsulation. This aligns with C#’s strongly-typed, object-oriented design in the .NET ecosystem, where access control is critical for robust applications.

1. Public Modifier

The public modifier allows unrestricted access to a member from anywhere, including outside the class, subclasses, or other assemblies.

  • Example:
    public class Person
    {
        public string Name { get; set; }
     
        public Person(string name)
        {
            Name = name;
        }
     
        public string Greet()
        {
            return $"Hello, {Name}!";
        }
    }
     
    class Program
    {
        static void Main()
        {
            Person person = new Person("Alice");
            Console.WriteLine(person.Name); // Output: Alice
            Console.WriteLine(person.Greet()); // Output: Hello, Alice!
        }
    }
    • How it Works: Name and Greet are accessible everywhere, enforced at both compile-time and runtime.

2. Private Modifier

The private modifier restricts access to within the same class, preventing access from subclasses or externally. This is enforced at both compile-time and runtime.

  • Example:
    public class Employee
    {
        private int salary;
     
        public Employee(int salary)
        {
            this.salary = salary;
        }
     
        public int GetSalary()
        {
            return salary; // OK: Accessed within class
        }
    }
     
    class Program
    {
        static void Main()
        {
            Employee emp = new Employee(50000);
            // Console.WriteLine(emp.salary); // Error: 'salary' is inaccessible due to its protection level
            Console.WriteLine(emp.GetSalary()); // Output: 50000
        }
    }
    • Runtime Enforcement: Attempting to access salary via reflection or other means without proper permissions will fail at runtime, unlike TypeScript’s bypass.

3. Protected Modifier

The protected modifier allows access within the class and its derived classes but not externally, enforced at both compile-time and runtime.

  • Example:
    public class Vehicle
    {
        protected int speed;
     
        public Vehicle(int speed)
        {
            this.speed = speed;
        }
     
        protected int GetSpeed()
        {
            return speed;
        }
    }
     
    public class Car : Vehicle
    {
        public Car(int speed) : base(speed) {}
     
        public string Describe()
        {
            return $"Speed: {speed}, accessed via {GetSpeed()}"; // OK: Protected members accessible
        }
    }
     
    class Program
    {
        static void Main()
        {
            Car car = new Car(120);
            Console.WriteLine(car.Describe()); // Output: Speed: 120, accessed via 120
            // Console.WriteLine(car.speed); // Error: 'speed' is inaccessible
        }
    }
    • How it Works: speed and GetSpeed are accessible in Car but not outside Vehicle or its subclasses.

4. Additional Modifiers (internal, protected internal, private protected)

C# offers additional modifiers not available in TypeScript:

  • internal: Accessible within the same assembly.
  • protected internal: Accessible within the same assembly or in derived classes (even in other assemblies).
  • private protected: Accessible only within the same class or derived classes in the same assembly.
  • Example: Internal and Protected Internal
    // Assembly1
    public class Base
    {
        internal string Company = "TechCorp";
        protected internal string Department = "IT";
    }
     
    public class Derived : Base
    {
        public string GetDetails()
        {
            return $"{Company}, {Department}"; // OK: Both accessible
        }
    }
     
    class Program
    {
        static void Main()
        {
            Base b = new Base();
            Console.WriteLine(b.Company); // OK: internal in same assembly
            Console.WriteLine(b.Department); // OK: protected internal
        }
    }
    • In Another Assembly:
      // Assembly2
      class External
      {
          void Test()
          {
              Base b = new Base();
              // Console.WriteLine(b.Company); // Error: 'Company' is inaccessible (internal)
              Console.WriteLine(b.Department); // OK: protected internal allows access in derived context
          }
      }
  • Example: Private Protected
    public class Base
    {
        private protected string Secret = "Hidden";
    }
     
    public class Derived : Base
    {
        public string GetSecret()
        {
            return Secret; // OK: Same assembly, derived class
        }
    }
     
    class Program
    {
        static void Main()
        {
            // Base b = new Base();
            // Console.WriteLine(b.Secret); // Error: Inaccessible
        }
    }

5. Compile-Time and Runtime Enforcement

C# enforces access modifiers at both compile-time (preventing invalid access in code) and runtime (restricting access via reflection or dynamic code unless explicitly allowed).

  • Example: Reflection Attempt
    using System.Reflection;
     
    public class Test
    {
        private string secret = "hidden";
    }
     
    class Program
    {
        static void Main()
        {
            Test test = new Test();
            FieldInfo field = typeof(Test).GetField("secret", BindingFlags.NonPublic | BindingFlags.Instance);
            // field.GetValue(test); // Runtime error: Security exception unless permissions granted
        }
    }
    • How it Works: Unlike TypeScript, C#’s runtime (CLR) enforces access restrictions, preventing unauthorized access even via reflection without proper permissions.

Key Differences Summarized with Examples

  1. Modifiers Available:

    • TypeScript: Supports public, private, and protected, with public as the default.
      class Test {
          public x: number = 1;
          private y: number = 2;
          protected z: number = 3;
      }
    • C#: Supports public, private, protected, internal, protected internal, private protected, with private as the default for class members.
      public class Test
      {
          public int x = 1;
          private int y = 2;
          protected int z = 3;
          internal string w = "internal";
      }
  2. Enforcement:

    • TypeScript: Access modifiers are enforced at compile-time only, erased in JavaScript, allowing runtime bypass.
      class Test {
          private secret: string = "hidden";
      }
      const test = new Test();
      console.log((test as any).secret); // Output: hidden
    • C#: Access modifiers are enforced at both compile-time and runtime, ensuring encapsulation.
      public class Test
      {
          private string secret = "hidden";
      }
      Test test = new Test();
      // Console.WriteLine(test.secret); // Error at compile-time
  3. Runtime Behavior:

    • TypeScript: No runtime access control, as modifiers are erased.
      class Test {
          private x: number = 10;
      }
      console.log(new Test()['x']); // Output: 10
    • C#: Runtime enforcement prevents unauthorized access, even via reflection without permissions.
      public class Test
      {
          private int x = 10;
      }
      // Reflection access would fail without permissions
  4. Protected Access:

    • TypeScript: protected allows access in subclasses, but external access is possible at runtime.
      class Base {
          protected x: number = 10;
      }
      class Derived extends Base {
          getX() { return this.x; }
      }
      console.log((new Derived() as any).x); // Output: 10
    • C#: protected restricts access to the class and subclasses, enforced at runtime.
      public class Base
      {
          protected int x = 10;
      }
      public class Derived : Base
      {
          public int GetX() => x;
      }
      // Console.WriteLine(new Derived().x); // Error
  5. Additional Modifiers:

    • TypeScript: No equivalent to C#’s internal, protected internal, or private protected.
      // No internal modifier
      class Test {
          private x: number = 10; // Closest to internal, but not assembly-scoped
      }
    • C#: Offers assembly-scoped modifiers for fine-grained control.
      public class Test
      {
          internal int x = 10; // Accessible within assembly
      }
  6. Constructor Shorthand:

    • TypeScript: Supports access modifiers in constructor parameters for concise property declaration.
      class Test {
          constructor(private x: number) {}
      }
    • C#: No direct constructor shorthand for properties, but auto-implemented properties reduce boilerplate.
      public class Test
      {
          public int X { get; }
          public Test(int x) => X = x;
      }

Summary

  • TypeScript Access Modifiers: public, private, and protected are enforced at compile-time only, aligning with JavaScript’s dynamic nature. They are erased in the compiled JavaScript, allowing runtime bypass of access restrictions via type coercion or direct property access. This makes TypeScript suitable for web development, where compile-time type safety is prioritized over runtime encapsulation.
  • C# Access Modifiers: public, private, protected, internal, protected internal, and private protected are enforced at both compile-time and runtime, ensuring strict encapsulation in the .NET ecosystem. Runtime enforcement prevents unauthorized access, even via reflection, making C# ideal for enterprise applications requiring robust security and encapsulation.

These differences reflect TypeScript’s focus on enhancing JavaScript with type safety for dynamic environments, versus C#’s emphasis on rigorous access control in a strongly-typed, runtime-enforced framework.