This TypeScript error occurs when you try to use JavaScript's private field syntax (#field) outside of a class declaration. Private fields with the # prefix must be declared within a class body and cannot be used in standalone functions, global scope, or outside their defining class. The error typically appears when migrating JavaScript code to TypeScript or when mixing class-based and functional programming patterns.
TypeScript enforces JavaScript's ECMAScript private field syntax rules strictly. When you see the error "Private field '#x' must be declared in an enclosing class," it means you're attempting to use a private field identifier (starting with #) in a context where private fields are not allowed. Private fields in JavaScript/TypeScript are a language feature introduced in ES2022 that provide true runtime privacy for class properties. Unlike TypeScript's `private` keyword (which is compile-time only), the # syntax creates fields that are inaccessible outside their class even after compilation. The key restriction is that #privateField syntax can only be used: 1. Within a class declaration (to declare the field) 2. Within instance methods of that same class (to access the field) 3. Within static methods of that class (for static private fields) You cannot use #privateField syntax: - Outside any class (global scope, module scope) - In standalone functions - In arrow functions defined outside the class - In object literals - In interfaces or type definitions This error often appears when: - You have a function that tries to access a private field from outside its class - You're using destructuring with private fields - You have callback functions that reference private fields - You're trying to export or import private fields directly
If you have a function that needs to access private fields, define it as a class method instead of a standalone function:
// BEFORE: Error - private field outside class
class User {
#password: string;
constructor(password: string) {
this.#password = password;
}
}
// Standalone function causing error
const validatePassword = (user: User) => {
return user.#password.length > 8; // Error: Private field '#password' must be declared...
};
// AFTER: Move function inside class as method
class User {
#password: string;
constructor(password: string) {
this.#password = password;
}
// Now a class method - can access #password
validatePassword(): boolean {
return this.#password.length > 8; // OK
}
}
const user = new User("secret123");
console.log(user.validatePassword()); // OK: trueClass methods have access to private fields through the this context.
If you need to pass a method as a callback, use bind() to preserve the this context:
class Timer {
#startTime: number;
constructor() {
this.#startTime = Date.now();
}
// Class method that accesses private field
getElapsed(): number {
return Date.now() - this.#startTime;
}
}
const timer = new Timer();
// BEFORE: Error - loses 'this' context
setTimeout(timer.getElapsed, 1000); // Error: Private field '#startTime'...
// AFTER: Bind the method to preserve context
setTimeout(timer.getElapsed.bind(timer), 1000); // OK
// Or use arrow function that captures 'this'
setTimeout(() => timer.getElapsed(), 1000); // Also OKBinding ensures the method executes with the correct this context, allowing private field access.
If you're using arrow functions as properties (common in React), define them as class methods or use property initializers:
// BEFORE: Error in React component
class MyComponent extends React.Component {
#state = { count: 0 };
// Arrow function property - still has access issues
handleClick = () => {
this.#state.count++; // May cause context issues
this.forceUpdate();
};
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
// AFTER: Use class method with bind in constructor
class MyComponent extends React.Component {
#state = { count: 0 };
constructor(props: any) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
// Regular class method
handleClick() {
this.#state.count++; // OK - proper 'this' context
this.forceUpdate();
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
// OR: Use TypeScript's property initializer syntax
class MyComponent extends React.Component {
#state = { count: 0 };
// This works correctly with private fields
handleClick = (): void => {
this.#state.count++; // OK - arrow function captures 'this'
this.forceUpdate();
};
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}Arrow function properties capture this at definition time, which usually works with private fields.
Private fields cannot be destructured. Use getter methods instead:
class Config {
#apiKey: string;
#endpoint: string;
constructor(apiKey: string, endpoint: string) {
this.#apiKey = apiKey;
this.#endpoint = endpoint;
}
// Public getters for controlled access
getApiKey(): string {
return this.#apiKey;
}
getEndpoint(): string {
return this.#endpoint;
}
}
const config = new Config("key-123", "https://api.example.com");
// BEFORE: Error - cannot destructure private fields
const { #apiKey, #endpoint } = config; // Error
// AFTER: Use getter methods
const apiKey = config.getApiKey(); // OK
const endpoint = config.getEndpoint(); // OK
// OR: Create a public interface for the data you need
interface ConfigData {
apiKey: string;
endpoint: string;
}
class Config {
#apiKey: string;
#endpoint: string;
constructor(apiKey: string, endpoint: string) {
this.#apiKey = apiKey;
this.#endpoint = endpoint;
}
// Method that returns public data structure
getConfigData(): ConfigData {
return {
apiKey: this.#apiKey,
endpoint: this.#endpoint,
};
}
}
const config = new Config("key-123", "https://api.example.com");
const { apiKey, endpoint } = config.getConfigData(); // OKPrivate fields are fundamentally incompatible with destructuring syntax.
If you don't need runtime privacy, consider using TypeScript's private keyword which has fewer restrictions:
// Using JavaScript private fields (#) - strict rules
class SecureClass {
#secret: string;
constructor(secret: string) {
this.#secret = secret;
}
// Can only access #secret in class methods
getSecret(): string {
return this.#secret;
}
}
// Using TypeScript 'private' keyword - more flexible
class FlexibleClass {
private secret: string;
constructor(secret: string) {
this.secret = secret;
}
// Can use in more contexts (though still compile-time only)
createLogger() {
// This works with 'private' but not with #
return () => console.log(this.secret);
}
}
const flexible = new FlexibleClass("hidden");
const logger = flexible.createLogger();
logger(); // OK: "hidden"
// Key differences:
// #privateField: Runtime privacy, strict access rules
// private field: Compile-time only, more flexible usage
// Choose based on your needsTypeScript's private keyword is removed during compilation, allowing more flexible patterns.
Sometimes the best solution is to redesign your code to avoid needing private field access in difficult contexts:
// BEFORE: Complex callback chain with private field access
class DataProcessor {
#data: number[];
constructor(data: number[]) {
this.#data = data;
}
processWithCallback(callback: (value: number) => void) {
// Problem: callback tries to access #data
this.#data.forEach(value => {
// Can't use #data here if callback needs it
callback(value);
});
}
}
// AFTER: Pass needed data as parameters
class DataProcessor {
#data: number[];
constructor(data: number[]) {
this.#data = data;
}
processWithCallback(callback: (value: number, metadata: string) => void) {
const metadata = this.#getMetadata(); // Compute inside class
this.#data.forEach(value => {
callback(value, metadata); // Pass data as parameter
});
}
#getMetadata(): string {
return `Processing ${this.#data.length} items`;
}
}
// OR: Use a different architecture
class DataProcessor {
#data: number[];
constructor(data: number[]) {
this.#data = data;
}
// Return processed data instead of using callbacks
process(): ProcessedData[] {
return this.#data.map(value => ({
value,
metadata: this.#getMetadata(),
timestamp: Date.now(),
}));
}
#getMetadata(): string {
return `Processing ${this.#data.length} items`;
}
}By designing APIs that don't require external code to access private fields directly, you avoid these errors entirely.
### JavaScript Private Fields vs TypeScript Private Keyword
JavaScript Private Fields (#):
- Introduced in ES2022 (ES2022 private fields)
- Provide true runtime privacy
- Cannot be accessed outside class even with type casting
- Must be declared in class body
- Cannot be accessed via this["#field"] or any dynamic access
- Work with inheritance (subclasses get their own separate private fields)
- Can have static private fields: static #count = 0;
TypeScript Private Keyword:
- Compile-time only feature
- Removed during compilation to JavaScript
- Can be bypassed with type casting: (instance as any).privateField
- More flexible usage patterns
- Compatible with older JavaScript versions
### When to Use Each
Use # private fields when:
- You need true runtime privacy
- Security is critical (API keys, tokens, passwords)
- You're targeting modern environments (ES2022+)
- You want to prevent accidental access even in JavaScript
Use TypeScript `private` when:
- You need compatibility with older JavaScript
- You're using patterns that require more flexibility
- Runtime privacy isn't critical
- You're writing library code consumed by JavaScript projects
### Common Pitfalls with # Private Fields
1. Inheritance issues: Each class in hierarchy gets its own private field, even with same name:
class Parent {
#value = "parent";
getValue() {
return this.#value;
}
}
class Child extends Parent {
#value = "child"; // Different field, not overriding
}
const child = new Child();
console.log(child.getValue()); // "parent" not "child"2. No dynamic access: Cannot use bracket notation:
class Example {
#field = "value";
getField(key: string) {
return this[key]; // Error with #field
return this.#field; // Only way
}
}3. JSON serialization: Private fields are not included in JSON.stringify():
class User {
#password = "secret";
public name = "John";
toJSON() {
return { name: this.name }; // Must manually include public fields
}
}### TypeScript Configuration
Ensure your tsconfig.json supports private fields:
{
"compilerOptions": {
"target": "ES2022", // or higher
"lib": ["ES2022", "DOM"],
// OR for older targets with polyfills:
"target": "ES2015",
"lib": ["ES2022", "DOM"]
}
}For older targets, TypeScript will downlevel private fields to WeakMaps, which may have performance implications.
### Debugging Private Fields
Since private fields are truly private, debugging can be challenging:
- Use Chrome DevTools: Private fields appear with # prefix in console
- Use getter methods for inspection during development
- Consider temporary public properties for debugging
- Use console.log({ ...instance }) - private fields won't appear
### Performance Considerations
- # private fields are generally fast in modern engines
- When downleveled to ES2015, they use WeakMaps which have some overhead
- Accessing private fields is slightly slower than public properties
- For performance-critical code, benchmark both approaches
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