阿里云主机折上折
  • 微信号
Current Site:Index > Private fields and naming conventions

Private fields and naming conventions

Author:Chuan Chen 阅读数:30416人阅读 分类: TypeScript

The Concept and Purpose of Private Fields

Private fields in TypeScript are members of a class that can only be accessed within the class itself. Their primary purpose is to encapsulate internal implementation details, preventing external code from directly modifying or relying on these details. Private fields are declared using the private modifier:

class Person {
  private _age: number;
  
  constructor(age: number) {
    this._age = age;
  }
  
  getAge(): number {
    return this._age;
  }
}

const person = new Person(30);
console.log(person._age); // Error: Property '_age' is private and only accessible within class 'Person'

The ECMAScript specification also introduces native private field syntax, using the # prefix:

class Person {
  #age: number;
  
  constructor(age: number) {
    this.#age = age;
  }
}

Naming Conventions and Rules

The TypeScript community has some common conventions for naming private fields:

  1. Underscore Prefix: The most traditional naming style
private _internalValue: string;
  1. No Prefix with Private Modifier: A TypeScript-specific approach
private internalValue: string;
  1. Hash Prefix: ES standard private fields
#internalValue: string;

For accessor methods, common naming patterns include:

class Temperature {
  private _celsius: number = 0;
  
  get celsius(): number {
    return this._celsius;
  }
  
  set celsius(value: number) {
    this._celsius = value;
  }
}

Comparison of Different Private Field Implementations

TypeScript offers multiple ways to implement private fields, each with its pros and cons:

  1. Compile-Time Private (private modifier)
class Example {
  private internal: string;
}
  • Pros: Simple syntax
  • Cons: Only checked at compile time; accessible at runtime
  1. WeakMap Private Pattern
const _internal = new WeakMap();

class Example {
  constructor() {
    _internal.set(this, 'secret');
  }
}
  • Pros: Truly private at runtime
  • Cons: Complex syntax, performance overhead
  1. ES Private Fields (# prefix)
class Example {
  #internal: string;
}
  • Pros: Truly private at runtime, relatively simple syntax
  • Cons: Requires newer JavaScript engine support

Practical Application Examples

Consider the implementation of a bank account class:

class BankAccount {
  private _balance: number = 0;
  #accountNumber: string;
  
  constructor(initialBalance: number, accountNumber: string) {
    this._balance = initialBalance;
    this.#accountNumber = accountNumber;
  }
  
  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error('Deposit amount must be greater than zero');
    }
    this._balance += amount;
  }
  
  get balance(): number {
    return this._balance;
  }
  
  get maskedAccountNumber(): string {
    return this.#accountNumber.slice(-4).padStart(this.#accountNumber.length, '*');
  }
}

Type System and Private Fields

TypeScript's type checking has special handling for private fields:

class Base {
  private x = 1;
}

class Derived extends Base {
  private x = 2; // Error: Property 'x' is private in type 'Derived'
}

class Different {
  private x = 3;
}

let base: Base = new Different(); // Error: Types are incompatible

Private Fields in Design Patterns

Using private fields to protect observer lists in the Observer pattern:

class Subject {
  private observers: Observer[] = [];
  #state: number;
  
  attach(observer: Observer): void {
    this.observers.push(observer);
  }
  
  setState(state: number): void {
    this.#state = state;
    this.notify();
  }
  
  private notify(): void {
    for (const observer of this.observers) {
      observer.update(this.#state);
    }
  }
}

Handling Private Fields in Testing

Special handling is required when testing private fields:

class MyClass {
  private _value = 42;
  
  getValue(): number {
    return this._value;
  }
}

// Test code
describe('MyClass', () => {
  it('should access private field', () => {
    const instance = new MyClass();
    expect((instance as any)._value).toBe(42);
  });
});

A better approach is to use type assertions:

interface MyClassWithPrivate {
  _value: number;
}

const instance = new MyClass() as unknown as MyClassWithPrivate;
expect(instance._value).toBe(42);

Performance Considerations

Different private field implementations have varying performance characteristics:

  1. Traditional underscore-prefixed fields are the fastest to access
  2. ES private fields (#) are well-optimized in modern engines
  3. WeakMap implementation has the worst performance
// Performance test example
class PerformanceTest {
  public publicField = 0;
  private _privateField = 0;
  #hashPrivateField = 0;
  
  testPublic() {
    for (let i = 0; i < 1e6; i++) {
      this.publicField += i;
    }
  }
  
  testPrivate() {
    for (let i = 0; i < 1e6; i++) {
      this._privateField += i;
    }
  }
  
  testHashPrivate() {
    for (let i = 0; i < 1e6; i++) {
      this.#hashPrivateField += i;
    }
  }
}

Interoperability with JavaScript

Considerations when TypeScript private fields interact with JavaScript:

// TypeScript class
class TSClass {
  private secret = 'typescript';
}

// JavaScript code
const instance = new TSClass();
console.log(instance.secret); // Accessible at runtime, but TypeScript will error

ES private fields are truly inaccessible in JavaScript:

class ESClass {
  #secret = 'ecmascript';
}

const esInstance = new ESClass();
console.log(esInstance.#secret); // Syntax error

Decorators and Private Fields

Decorators can enhance private field functionality:

function logAccess(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalGet = descriptor.get;
  descriptor.get = function() {
    console.log(`Accessing ${propertyKey}`);
    return originalGet?.call(this);
  };
}

class User {
  private _name: string;
  
  constructor(name: string) {
    this._name = name;
  }
  
  @logAccess
  get name(): string {
    return this._name;
  }
}

Inheritance and Private Fields

Behavior of private fields in inheritance:

class Parent {
  private parentSecret = 'parent';
  #realSecret = 'really private';
  
  revealSecret(): string {
    return this.parentSecret;
  }
}

class Child extends Parent {
  private parentSecret = 'child'; // Allowed because parent's parentSecret is invisible to this class
  
  tryReveal(): string {
    return this.#realSecret; // Error: Property '#realSecret' is private in class 'Parent'
  }
}

Interfaces and Private Fields

Interfaces cannot define private fields but can require implementing classes to have specific private fields:

interface IHasSecret {
  getSecret(): string;
}

class SecretKeeper implements IHasSecret {
  private _secret = 'confidential';
  
  getSecret(): string {
    return this._secret;
  }
}

Module System and Private Fields

TypeScript's private modifier remains effective across different modules:

// moduleA.ts
export class A {
  private internal = 123;
}

// moduleB.ts
import { A } from './moduleA';

const a = new A();
console.log(a.internal); // Error: Property 'internal' is private

Reflection and Private Fields

Reflection APIs can bypass TypeScript's private restrictions:

class PrivateHolder {
  private secret = 'hidden';
}

const instance = new PrivateHolder();
console.log(Reflect.get(instance, 'secret')); // Output: 'hidden'

Common Mistakes and Pitfalls

  1. Accidentally exposing private fields:
class Oops {
  private _value = 42;
  
  getValue() {
    return this; // Accidentally returns the entire instance
  }
}

const oops = new Oops();
console.log(oops.getValue()._value); // Can access private field
  1. JSON serialization issues:
class User {
  private password = 'secret';
  name = 'Alice';
}

const user = new User();
console.log(JSON.stringify(user)); // Output: {"name":"Alice"}

Toolchain Support

  1. TypeScript compiler options:
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictPropertyInitialization": true
  }
}
  1. ESLint rules:
{
  "rules": {
    "@typescript-eslint/explicit-member-accessibility": ["error"],
    "@typescript-eslint/naming-convention": [
      "error",
      {
        "selector": "property",
        "modifiers": ["private"],
        "format": ["camelCase"],
        "leadingUnderscore": "allow"
      }
    ]
  }
}

Historical Evolution and Future Directions

The evolution of TypeScript private fields:

  1. Early versions: Only the private modifier
  2. TypeScript 3.8: Support for ECMAScript private field syntax
  3. Future possibilities: Stricter runtime private checks
// TypeScript private fields
class OldWay {
  private legacy = 'old';
}

// ES private fields
class NewWay {
  #modern = 'new';
}

Comparison with Other Languages

  1. Java:
class JavaExample {
  private int value;
}
  1. C#:
class CSharpExample {
  private string _value;
}
  1. Python (convention-based, not enforced):
class PythonExample:
    def __init__(self):
        self._protected = 'convention'
        self.__private = 'name mangled'

Best Practices in Large Projects

Recommendations for large TypeScript projects:

  1. Consistently use one private field style (recommend ES private fields)
  2. Use private fields for true internal state
  3. Use protected for members that subclasses need to access
  4. Establish team naming convention documentation
// Recommended project style
class ProjectStandard {
  // Public API
  public apiMethod() {}
  
  // Accessible to subclasses
  protected internalMethod() {}
  
  // Truly private implementation details
  #coreLogic() {}
  
  // TypeScript private, used for team conventions
  private _legacyReason = 'migration';
}

Compilation Output Differences

Differences in compiled JavaScript for different private field syntaxes:

  1. TypeScript private:
class A {
  private x = 1;
}

Compiles to:

class A {
  constructor() {
    this.x = 1;
  }
}
  1. ES private:
class B {
  #x = 1;
}

Compiles to:

class B {
  constructor() {
    _x.set(this, 1);
  }
}
const _x = new WeakMap();

Debugging and Private Fields

Chrome DevTools support for private fields:

  1. ES private fields display as #fieldName
  2. TypeScript private fields display as regular properties
  3. Private field values can be inspected during breakpoint debugging
class DebugExample {
  private tsPrivate = 'typescript';
  #esPrivate = 'ecmascript';
  
  debug() {
    debugger; // Inspect variables here
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.