Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any way to delegate the index property in typescript?

Tags:

typescript

Is there any way to delegate the index property to a member in typescript? I am writing a wrapper, and I would like to delegate the index property to the object I am wrapping.

Something like:

interface MyStringArray {
    length : number;
    clear() : void;
    [index: number] : string;
}

export class MyStringArrayWrapper implements MyStringArray {

    private wrapped : MyStringArray;

    public get length() : number { 
        return this.wrapped.length;
    }

    public clear() : void {
        this.wrapped.clear();
    }

    // This doesn't work
    public get [index : number] : string { 
        return this.wrapped[index];
    }

    // This doesn't work either
    public set [index : number](value: string) { 
        this.wrapped[index] = value;
    }
}
like image 591
afeygin Avatar asked Aug 12 '16 19:08

afeygin


1 Answers

TypeScript does not allow for this because it cannot generate JavaScript code for such a construct. There's no way in JavaScript to define a catch-all getter/setter on a class that is invoked whenever any property is set.

The closest I could get is by defining hasItem,getItem and setItem methods on the class, and then wrapping every instance of the prototype of that class in a ES2015 Proxy object which proxies all gets and sets to properties of that instance.

function isArrayIndex(key) {
    var keyAsNumber = +key; // convert string to number
    if (keyAsNumber < 0) {
        return false; // must be positive or zero
    }
    if (keyAsNumber !== keyAsNumber|0) {
        return false; // must be integer
    }
    return true;
}

function toArrayIndex(key) {
    return key|0; // convert string to integer
}

function MyArrayWrapper(wrapped) {
    this.wrapped = wrapped;
}
  
MyArrayWrapper.prototype = {
    get length() { 
        return this.wrapped.length;
    },

    clear() {
        this.wrapped.length = 0;
    },
  
    getItem(index) {
        return this.wrapped[index];
    },
  
    setItem(index, value) {
        this.wrapped[index] = value;
    }
  
}

var MyArrayWrapperProxyHandler = {
    get: function (target, key, receiver) {
        if (isArrayIndex(key)) {
            return receiver.getItem(toArrayIndex(key));
        } else {
            return Reflect.get(target, key, receiver);
        }
    },
    set: function (target, key, value, receiver) {
        if (isArrayIndex(key)) {
            return receiver.setItem(toArrayIndex(key), value);
        } else {
            return Reflect.set(target, key, value, receiver);
        }
    }
};

MyArrayWrapper.prototype = new Proxy(MyArrayWrapper.prototype, MyArrayWrapperProxyHandler);

var array = ['a', 'b', 'c'];
var wrapper = new MyArrayWrapper(array);

console.log('read:', wrapper[2] === 'c'); // true, reads from wrapped array

wrapper[3] = 'd';
console.log('write:', array[3] === 'd'); // true, writes to wrapped array
console.log('resize:', wrapper.length === 4); // true, wrapped array is resized

This requires a modern browser with support for Proxy objects, and it uses Reflect to manually pass other properties (e.g. this.wrapped) through the prototype chain. There's a lot of stuff that doesn't work (e.g. '2' in wrapped is false) since I didn't find a way to trap those correctly.

Moreover, proxies are much slower than directly using hasItem/getItem/setItem, so I don't recommend using them for anything performance critical. They can also be very confusing to use, since suddenly a simple foo[0] = 'bar' can do all sorts of crazy stuff when there are proxies involved.

If you're okay with all of the above, then sure, add some types to the above snippet and you have TypeScript code with a delegate array index getter/setter. This is more of a 'proof of concept' really, I just wanted to see how far you could push JavaScript. :-P

like image 186
Mattias Buelens Avatar answered Oct 06 '22 02:10

Mattias Buelens