Can't access private fields from within the same class

291 Views Asked by At

I'm trying to enhance the Map class in JavaScript by adding a TypedMap subclass that extends the Map superclass.

TypeMap.js

export default class TypedMap extends Map {
  #keyType;
  #valueType;
  constructor(keyType, valueType, entries) {
    if (entries) {
      for (let [k, v] of entries) {
        if (typeof k !== keyType || typeof v !== valueType) {
          throw new TypeError(`Wrong type for entry [${k}, ${v}]`);
        }
      }
    }

    super(entries);

    this.#keyType = keyType;
    this.#valueType = valueType;
  }

  set(key, value) {
    if (this.#keyType && typeof key !== this.#keyType) {
      throw new TypeError(`${key} is not of type ${this.#keyType}`);
    }

    if (this.#valueType && typeof value !== this.#valueType) {
      throw new TypeError(`${value} is not of type ${this.#valueType}`);
    }

    return super.set(key, value);
  }
}

main.js

import TypedMap from './TypedMap.js';

let entries = [
  [1, 2],
  [3, 4],
  [5, 6],
];

let typedMap = new TypedMap('number', 'number', entries);

Error I'm getting

Uncaught TypeError: Cannot read private member #keyType from an object whose class did not declare it
    at TypedMap.set (TypedMap.js?t=1696367023223:20:14)
    at new Map (<anonymous>)
    at new TypedMap (TypedMap.js?t=1696367023223:13:5)
    at main.js?t=1696367092683:9:16

The #keyType and #valueType fields are private but I should still access them from within the class TypedMap but somehow that doesn't happen to be the case here.

I think it has something to do with the overridden set method because I added a test method in the TypedMap class and could access the private fields.

Can anyone explain what is happening here?

2

There are 2 best solutions below

5
Alexander Nenashev On BEST ANSWER

You have the problem that you call super(entries) which calls this.set() which needs your private properties. But they aren't defined yet in this.

The error message in Chrome is kind of misleading, in Firefox it's more meaningful:

TypeError: can't access private field or method: object is not the right class

That could mean that this isn't in a proper state ("not the right class"): the private properties are declared but not yet defined.

On the other hand you cannot access this before calling super() to define your private props since super() actually provides a proper this with a proper prototype chain.

So it's a deadlock. So add your entries manually.

Btw you can remove your type checking in the constructor since it's done in set().

class TypedMap extends Map {
    
    #keyType;
    #valueType;
    
  constructor(keyType, valueType, entries = []) {
    super();
    this.#keyType = keyType;
    this.#valueType = valueType;
    entries.forEach(entry => this.set(...entry));
  }

  set(key, value) {
  
    if (this.#keyType && typeof key !== this.#keyType) {
      throw new TypeError(`${key} is not of type ${this.#keyType}`);
    }

    if (this.#valueType && typeof value !== this.#valueType) {
      throw new TypeError(`${value} is not of type ${this.#valueType}`);
    }
    
    return super.set(key, value);

  }
}
let entries = [
  [1, 2],
  [3, 4],
  [5, 6],
];

let typedMap = new TypedMap('number', 'number', entries);

typedMap.forEach((k,v)=>console.log(JSON.stringify([k,v])));

// check it throws
new TypedMap('number', 'string', entries);

1
Peter Seliger On

Note, and quoting from MDN ...

"In the constructor body of a derived class (with extends), the super keyword may appear as a "function call" (super(...args)), which must be called before the this keyword is used, and before the constructor returns. It calls the parent class's constructor and binds the parent class's public fields, after which the derived class's constructor can further access and modify this."

And adding to the already provided answer of Alexander Nenashev ... the OP might think about implementing TypedMap with just an additionally provided single validator function.

The advantage comes with more flexibility of how to handle types in terms of ...

  • allowed/valid types, where one separately for each entry value (either key or value) could easily implement checks for multiple valid types.

Furthermore, any implementation should also just extend the arguments signature of the extended type. Thus as for TypedMap versus Map one would choose for the former a signature of ...

  • constructor(entries, keyType, valueType) { ... } in case of the OP's examlpe code,

  • constructor(entries, validateEntry) { ... } in case of the here proposed approach.

class TypedMap extends Map {
  #validateEntry;

  constructor(entries, validateEntry) {
    super();

    this.#validateEntry = ('function' === typeof validateEntry)
      && validateEntry
      || (() => true);

    [...entries]
      .forEach(entry =>
        this.set(entry)
      );
  }
  set(entry) {
    const [key, value] = entry;

    if (this.#validateEntry(key, value)) {
      super.set(key, value);
    }
    return this;
  }
}

// a possible validator function for a custom `TypedMap` instance.
function validateEntry(key, value) {

  // - is allowed to throw, but is not supposed or expected to do so.
  // - has to always return a boolean value ...
  //    - ... in case of throwing, just return `true` for the opposite,
  //    - ... otherwise return either of the boolean values `true` or `false`.

  if ((typeof key !== 'string') && (typeof key !== 'number'))  {
    throw new TypeError(
      "Regarding the 'key' item of 'set'; 'key' is expected to be either a string or a number type."
    );
  }
  if (typeof value !== 'string') {
    throw new TypeError(`Regarding the 'value' item of 'set'; ${ value } is not a string value.`);
  }
  return true;
}

const entries = [
  [1_111, 'quick'],
  ['foo', 'brown'],
  [3_333, 'fox'],
];
const typedMap = new TypedMap(entries, validateEntry);

console.log(
  "entries of 'typedMap' ...", [...typedMap]
);
console.log(
  "return type of a valid 'set' operation ...",
  new TypedMap([['bar', '1_000']], validateEntry)?.constructor?.name,
);

console.log(
  "countercheck the handling of an invalid entry by 'set' ...",
);
// countercheck the handling of an invalid entry by 'set'.
new TypedMap([['baz', 10_000]], validateEntry);
.as-console-wrapper { min-height: 100%!important; top: 0; }