This TypeScript error occurs when you attempt to access a private class member from outside the class definition. TypeScript's private modifier restricts visibility to within the class itself, preventing external code from accessing private properties or methods.
TypeScript's access modifiers control the visibility of class members. The `private` modifier ensures that a property, method, or accessor is only accessible within the class where it's defined. When you try to access a private member from outside the class—whether from another class, a subclass, or external code—TypeScript's type checker throws error TS2341. This is a compile-time protection mechanism. TypeScript enforces encapsulation by restricting access to internal implementation details, helping you write more maintainable and secure code. The private modifier signals that a member is an internal implementation detail that should not be accessed or relied upon by external code. It's important to understand that TypeScript's `private` keyword only provides compile-time protection. When TypeScript is transpiled to JavaScript, the `private` modifier is stripped away, and the property becomes accessible at runtime. For true runtime privacy, you need to use JavaScript's native private fields (denoted with `#`) introduced in ES2022.
If the property should be accessible from outside the class, change it from private to public (or remove the modifier, as public is the default):
// BEFORE: Error TS2341
class User {
private email: string;
constructor(email: string) {
this.email = email;
}
}
const user = new User("[email protected]");
console.log(user.email); // Error: Property 'email' is private
// AFTER: Make it public
class User {
public email: string; // or just: email: string;
constructor(email: string) {
this.email = email;
}
}
const user = new User("[email protected]");
console.log(user.email); // OK: "[email protected]"Only use this approach if the property truly needs to be externally accessible.
If you want subclasses to access the property but keep it private from external code, use protected instead of private:
// BEFORE: Error in subclass
class Animal {
private name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} barks!`); // Error: 'name' is private
}
}
// AFTER: Use protected
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} barks!`); // OK: protected allows subclass access
}
}
const dog = new Dog("Rex");
dog.bark(); // OK: "Rex barks!"
console.log(dog.name); // Still Error: protected is not externally accessibleThis is the middle ground between private and public.
Keep the property private and provide controlled read access through a public getter method:
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
// Public getter method
getBalance(): number {
return this.balance;
}
// Public method to modify balance (controlled)
deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
}
}
}
const account = new BankAccount(1000);
console.log(account.balance); // Error: balance is private
console.log(account.getBalance()); // OK: 1000
account.deposit(500);
console.log(account.getBalance()); // OK: 1500This approach maintains encapsulation while providing controlled access.
Use TypeScript's get and set accessors for a property-like syntax with private backing fields:
class User {
private _email: string;
constructor(email: string) {
this._email = email;
}
// Getter accessor
get email(): string {
return this._email;
}
// Setter accessor with validation
set email(value: string) {
if (!value.includes("@")) {
throw new Error("Invalid email format");
}
this._email = value;
}
}
const user = new User("[email protected]");
console.log(user.email); // OK: "[email protected]" (calls getter)
user.email = "[email protected]"; // OK: sets via setter
user.email = "invalid"; // Throws error: Invalid email format
console.log(user._email); // Error: _email is privateThis approach lets you use property syntax while maintaining validation and encapsulation.
If you need to work with the private property, move the logic into a public method of the class:
class Timer {
private startTime: number;
constructor() {
this.startTime = Date.now();
}
// Public method that uses private property internally
getElapsedSeconds(): number {
return (Date.now() - this.startTime) / 1000;
}
reset(): void {
this.startTime = Date.now();
}
}
const timer = new Timer();
console.log(timer.startTime); // Error: startTime is private
console.log(timer.getElapsedSeconds()); // OK: 0.123
timer.reset(); // OK: resets internal startTimeThis keeps internal state private while exposing necessary functionality through public methods.
If you need true runtime privacy that persists after compilation, use JavaScript's native private fields with the # prefix:
class SecureData {
#apiKey: string; // JavaScript private field
constructor(key: string) {
this.#apiKey = key;
}
validateKey(input: string): boolean {
return input === this.#apiKey;
}
}
const data = new SecureData("secret-123");
console.log(data.#apiKey); // Error at compile-time AND runtime
console.log((data as any).#apiKey); // Still error at runtime
console.log(data.validateKey("secret-123")); // OK: trueDifferences between `private` and `#`:
- private keyword: Compile-time only, stripped in JavaScript output
- # private fields: Runtime enforcement, truly inaccessible even with type casting
Use # when you need hard privacy guarantees that cannot be bypassed.
### Compile-Time vs Runtime Privacy
TypeScript's private modifier is a compile-time construct only. After TypeScript is compiled to JavaScript, the modifier is completely removed:
// TypeScript source
class Example {
private secret: string = "hidden";
}
// Compiled JavaScript output (ES2015+)
class Example {
constructor() {
this.secret = "hidden"; // No longer "private"
}
}
// At runtime, anyone can access it
const ex = new Example();
console.log((ex as any).secret); // Works: "hidden" (casting bypasses TypeScript)
console.log(ex["secret"]); // Works in plain JavaScriptFor true runtime privacy, use ECMAScript private fields (#):
class Example {
#secret: string = "hidden";
}
// Compiled JavaScript preserves the # syntax
// Cannot access #secret at runtime, even with casting or bracket notation### Private vs Protected vs Public
| Modifier | Class Itself | Subclasses | External Code |
|----------|--------------|------------|---------------|
| private | ✅ Yes | ❌ No | ❌ No |
| protected | ✅ Yes | ✅ Yes | ❌ No |
| public | ✅ Yes | ✅ Yes | ✅ Yes |
Best practice: Start with the most restrictive modifier (private) and only loosen as needed.
### When to Use Each Access Modifier
Use `private` when:
- The property is an internal implementation detail
- External code should never directly access it
- You want to prevent accidental misuse
Use `protected` when:
- Subclasses need access for inheritance
- External code should not access it
- You're building an extensible class hierarchy
Use `public` when:
- The property is part of the class's public API
- External code needs direct access
- The value has no validation or special handling
### TypeScript and JavaScript Interoperability
When writing library code that may be consumed by JavaScript projects, remember:
// In your TypeScript library
export class Config {
private apiKey: string;
constructor(key: string) {
this.apiKey = key;
}
}
// JavaScript consumer (after compilation)
const config = new Config("key-123");
console.log(config.apiKey); // Works! No compile-time checks in JSTo prevent this, use # private fields for real privacy, or document your API clearly.
### Static Private Members
Private modifiers also work with static members:
class Database {
private static instance: Database;
private constructor() {} // Private constructor (singleton pattern)
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
}
const db = new Database(); // Error: constructor is private
const db = Database.getInstance(); // OK: uses public static method### Parameter Properties Shorthand
TypeScript allows declaring and initializing private properties directly in the constructor:
// Verbose way
class User {
private name: string;
private email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
// Shorthand (parameter properties)
class User {
constructor(
private name: string,
private email: string
) {}
}Both produce the same result, but the shorthand is more concise.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript