skip to Main Content

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

Answers


  1. 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);
    Login or Signup to reply.
  2. Adding to the already provided answer …

    … 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; }
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search