Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A private member in TypeScript is a class property, method, or accessor prefixed with the # symbol, implementing the ECMAScript standard for strict, runtime encapsulation. Unlike TypeScript’s private access modifier, which only enforces visibility during static type checking, # private identifiers provide “hard privacy” and are completely inaccessible outside of the declaring class’s lexical scope at runtime.
class DataContainer {
    // Private fields (declaration is mandatory before assignment)
    #internalId: string;
    #metadata = { initialized: true };

    constructor(id: string) {
        this.#internalId = id;
    }

    // Private accessor
    get #idLength(): number {
        return this.#internalId.length;
    }

    // Private method
    #validateId(): boolean {
        return this.#idLength > 0;
    }

    public process(): void {
        if (this.#validateId()) {
            // Accessible only within the class body
            console.log(`Processing: ${this.#internalId}`);
        }
    }
}

const container = new DataContainer("sys-01");
// container.#internalId; 
// TypeScript Error: Property '#internalId' is not accessible outside class 'DataContainer' because it has a private identifier.

Technical Characteristics

  • Broad Applicability: The # syntax applies not just to fields, but also to class methods (#method() {}) and accessors (get #prop() {}).
  • Strict Lexical Scoping: The member is bound strictly to the class body where it is defined. It cannot be accessed by derived classes (subclasses) or external instances.
  • Runtime Inaccessibility: Private members cannot be bypassed using dynamic property access. While a TypeScript private property can be accessed at runtime using bracket notation (instance['propertyName']), attempting to access a private field via bracket notation (instance['#propertyName']) evaluates to undefined. It does not access the private field, nor does it throw an error; it simply searches for a standard public property with the literal string key "#propertyName".
  • Mandatory Upfront Declaration: You cannot dynamically add a private field to an instance. It must be explicitly declared in the class body.
  • Distinct Namespace: Private fields exist in a separate namespace from public properties. A class can simultaneously possess a public property and a private field with the same base identifier (e.g., value and #value).

Ergonomic Brand Checks (in Operator)

TypeScript supports ECMAScript ergonomic brand checks. The in operator can be used to safely determine if an object possesses a specific private field at runtime. This evaluates to a boolean without throwing a TypeError if the object is not an instance of the class.
class NetworkRequest {
    #requestToken: string;

    constructor(token: string) {
        this.#requestToken = token;
    }

    static isNetworkRequest(obj: any): obj is NetworkRequest {
        // Safely checks for the presence of the private field at runtime
        return #requestToken in obj;
    }
}

# vs. private Keyword

TypeScript supports two mechanisms for class member privacy. Understanding their mechanical differences is critical:
class PrivacyMechanics {
    private softPrivate: number = 1;
    #hardPrivate: number = 2;

    // 1. Constructor Parameter Properties
    // The 'private' keyword can automatically declare and assign a class field.
    // The '#' identifier CANNOT be used in parameter properties (results in a syntax error).
    constructor(private id: string) {
        // constructor(#id: string) // Syntax Error
    }
}

const instance = new PrivacyMechanics("test-id");

// 2. TypeScript 'private' modifier (Soft Privacy)
// Normal access fails at compile-time:
// instance.softPrivate; // TypeScript Error

// However, casting to 'any' bypasses the type checker, allowing successful runtime access:
console.log((instance as any).softPrivate); // Outputs: 1

// 3. ECMAScript '#' identifier (Hard Privacy)
// Normal access fails at compile-time:
// instance.#hardPrivate; // TypeScript Error

// Casting to 'any' still fails at compile-time for '#' identifiers:
// console.log((instance as any).#hardPrivate); 
// TypeScript Error: Property '#hardPrivate' is not accessible...

// Dynamic access attempts fail to retrieve the private value at runtime:
console.log((instance as any)['#hardPrivate']); // Outputs: undefined

Compilation and Target Environments

The emitted JavaScript for # private members depends heavily on the target specified in your tsconfig.json:
  • ES2022 and later: TypeScript emits the native ECMAScript # syntax directly, relying on the JavaScript engine’s native implementation.
  • ES2021 and earlier: TypeScript downlevels the private fields using a WeakMap implementation to guarantee runtime privacy without leaking memory.
// Conceptual representation of downleveled ES2015 emission
var _DataContainer_internalId = new WeakMap();

class DataContainer {
    constructor(id) {
        _DataContainer_internalId.set(this, void 0);
        _DataContainer_internalId.set(this, id);
    }
}
Master TypeScript with Deep Grasping Methodology!Learn More