javascriptnode.jstypescriptbufferuint8array

The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array. Received type number - When using Uint8Array.slice()


My code implementation is the following extension of the Uint8Array class.

export class ByteArray extends Uint8Array {
    ...

    private _encoded: string;

    ...

    constructor(_encoded: string) {
        super(Buffer.from(_encoded, "base64"));
        this._encoded = _encoded;
    }
}

I am getting the error:

TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type number (134)

Whenever I try to use this.slice(start, end). For example this.slice(4, 10);. Any help is greatly appreciated.


Solution

  • The problem is caused by your constructor and how it gets called by the implementation of .slice() for the Uint8Array. The definition of that .slice() method says that it will create a new TypedArray with a new Buffer under it and to do that, it calls the constructor of the object that the .slice() was called on. That would be the constructor of your ByteArray class since the object is a ByteArray. So, it calls your constructor for the ByteArray class.

    But, your constructor does not support the form of the Uint8Array constructor that .slice() tries to use. The Uint8Array constructor supports all of these forms:

    new Uint8Array(); // new in ES2017
    new Uint8Array(length);
    new Uint8Array(typedArray);
    new Uint8Array(object);
    
    new Uint8Array(buffer);
    new Uint8Array(buffer, byteOffset);
    new Uint8Array(buffer, byteOffset, length);
    

    Probably .slice() is using this last form which would cause your constructor to call:

     super(Buffer.from(_encoded, "base64"));
    

    But, _encoded in this case would be a buffer object, not the string that Buffer.from(_encoded, "base64") is expecting and thus the error you get.

    There are a couple ways to fix this:

    Detect Your Constructor Arguments, Pass Others Through

    You can fix it by passing through any constructor arguments that aren't just a single string to the regular constructor. But, this will create an object of your ByteArray class that does NOT have the _encoded property set on it. I'm not sure what you really want to do with the object that .slice() returns.

    Here's a version that works detects your particular constructor and otherwise passes things onto the Uint8Array constructor (normal Javascript, not TypeScript):

    class ByteArray extends Uint8Array {
        constructor(...args) {
            let _encoded = args[0];
            // see if constructor arguments are my string argument
            if (typeof _encoded === "string") {
                super(Buffer.from(_encoded, "base64"));
                this._encoded = _encoded;
            } else {
                // not my string, pass through all constructor arguments
                // to default constructor
                super(...args);
            }
        }
    }
    

    Note: This is a general issue when you sub-class an object that contains methods that create new versions of itself. Those methods will attempt to use the constructor of the existing object type and will then call that constructor to create the new object. Your constructor MUST support existing forms of constructor arguments for this to work because you don't know which form of constructor arguments these other methods may be using.

    Tell Uint8Array Methods to Create Uint8Array Objects

    Another way to fix this is to specify that you don't want .slice() or any other method called on your ByteArray to create a ByteArray object, but rather to create a Uint8Array. You can do that like this:

    class ByteArray extends Uint8Array {
        // when creating new objects from methods of this one,
        // make them regular Uint8Array objects, not ByteArray objects
        static get[Symbol.species]() { return Uint8Array; }
    
        constructor(_encoded) {
            super(Buffer.from(_encoded, "base64"));
        }
    }
    

    Then, when .slice() goes to create a new object, it will create a Uint8Array instead of your ByteArray and the constructor for Uint8Array will work normally. This means that the result of call .slice() on one of your ByteArray objects will not be a ByteArray object - it will be a Uint8Array instead.

    You can read about the Symbol.species property here on MDN.

    General Discussion

    The problem you ran into is a problem for sub-classing any object that has methods that attempt to create a new object of the same class as the one they were called on. For example, sub-classing an Array object would have the same issue with .slice(). To support full functionality of all these base class methods, you have to either support all the constructor arguments that the base class methods may use or you have to use the .species support to tell those base class methods that you don't want them to create objects of your sub-class, but rather of the base-class (so they won't call your constructor).