javascripttypescriptes6-proxy

How to correctly type a JavaScript proxy handler in TypeScript for (Angular Signals-like) reactive state management?


I wanted to create a short documentation and explain the principle of Angular Signals using JavaScript proxies in an example. Then the question came up: could I quickly write it in TypeScript and set up the types? I said, of course, no problem, but then, surprise—somehow the types didn't fit anymore.

How can I use the correct types e.g. in:

target[property as keyof StateType] = value as StateType[keyof StateType];

outputs an error:

Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.(2322)

This is the whole example (Online TS Playground):

type StateType = {
    count: number;
    name: string;
};

const state: StateType = {
    count: 0,
    name: 'React'
};

function onStateChange<K extends keyof StateType>(property: K, value: StateType[K]): void {
    console.log(`The property "${property}" was changed to "${value}"`);
}

const handler: ProxyHandler<StateType> = {
    set(target, property, value) {
        if (property in target) {
            target[property as keyof StateType] = value as StateType[keyof StateType];
            onStateChange(property as keyof StateType, value as StateType[keyof StateType]);
            return true;
        }
        return false;
    }
};

const proxyState = new Proxy(state, handler);

proxyState.count = 1; 
proxyState.name = 'Angular'; 

Solution

  • Okay, I've got it. I would have needed to spend another 15 minutes on it. So that others don’t run into the same problem, here is the solution (1st Attempt):

    type StateType = {
        count: number;
        name: string;
    };
    
    const state: StateType = {
        count: 0,
        name: 'React'
    };
    
    function onStateChange<K extends keyof StateType>(property: K, value: StateType[K]): void {
        console.log(`The property "${property}" was changed to "${value}"`);
    }
    
    const handler: ProxyHandler<StateType> = {
        set<K extends keyof StateType>(
            target: StateType,
            property: K,
            value: StateType[K]
        ): boolean {
            if (property in target) {
                target[property] = value;
                onStateChange(property, value);
                return true;
            }
            return false;
        }
    };
    
    const proxyState = new Proxy(state, handler);
    
    proxyState.count = 1;
    proxyState.name = 'Angular';
    

    Short Explanation:

    Generics in set Method ensures that prop and value both are correct typed due to StateType.

    Edit & Update (2nd attempt, simplified, Online TS Playground):

    type StateType = {
        count: number;
        name: string;
    };
    
    const state: StateType = {
        count: 0,
        name: 'React'
    };
    
    function onStateChange<K extends keyof StateType>(property: K, value: StateType[K]): void {
        console.log(`The property "${property}" was changed to "${value}"`);
    }
    
    const handler: ProxyHandler<StateType> = {
        set(
            target: StateType,
            property: string | symbol,
            value: any
        ): boolean {
            if (property in target) {
                const key = property as keyof StateType;
                (target[key] as any) = value;
                onStateChange(key, value as StateType[typeof key]);
                return true;
            }
            return false;
        }
    };
    
    const proxyState = new Proxy(state, handler);
    
    proxyState.count = 1; 
    proxyState.name = 'Angular';