Skip to content

Using object destructuring with ECMAScript's private field as computed property name leads to runtime error #37791

Closed
@adamgauthier

Description

@adamgauthier

TypeScript Version: 3.8.3

Search Terms: computed object property name destructuring private ECMAScript

Code

class PropertyAccessor {
    readonly #propertyName: string;

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

    getPropertyValue(obj: Record<string, string | undefined>): string | undefined {
        const { [this.#propertyName]: value } = obj;
        return value;
    }
}

const accessor = new PropertyAccessor('name');
console.log(
    accessor.getPropertyValue({ name: 'Adam' })
);

Expected behavior:
'Adam' is logged at runtime.

Actual behavior:
Runtime error occurs:

Private field '#propertyName' must be declared in an enclosing class 

Additional information:
I just started using TypeScript and I ran into this problem while converting one of my javascript projects, the behavior was confusing to me. It seems like the code is transpiled incorrectly, as it works when not using destructuring syntax:

const { [this.#propertyName]: value } = obj;
// compiles to
const { [(_propertyName = new WeakMap(), this.#propertyName)]: value } = obj;

const value = obj[this.#propertyName];
// compiles to
const value = obj[__classPrivateFieldGet(this, _propertyName)];

The error does not occur when using TypeScript's private field instead of ECMAScript's.
The error does not occur when targeting ESNext.

Related Issues: Maybe #26572 #26355? Doesn't seem like the same problem but similar keywords.

Output
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to get private field on non-instance");
    }
    return privateMap.get(receiver);
};
var _propertyName;
class PropertyAccessor {
    constructor(propertyName) {
        _propertyName.set(this, void 0);
        __classPrivateFieldSet(this, _propertyName, propertyName);
    }
    getPropertyValue(obj) {
        const { [(_propertyName = new WeakMap(), this.#propertyName)]: value } = obj;
        return value;
    }
}
const accessor = new PropertyAccessor('name');
console.log(accessor.getPropertyValue({ name: 'Adam' }));
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": 2,
    "target": "ES2017",
    "jsx": "React",
    "module": "ESNext"
  }
}

Playground Link: Provided

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issueHas ReproThis issue has compiler-backed repros: https://aka.ms/ts-reprosRescheduledThis issue was previously scheduled to an earlier milestone

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions