Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typescript can't infer types when using Proxy #20846

Open
justinbc820 opened this issue Dec 21, 2017 · 10 comments
Open

Typescript can't infer types when using Proxy #20846

justinbc820 opened this issue Dec 21, 2017 · 10 comments
Labels
Bug A bug in TypeScript Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript Help Wanted You can do this
Milestone

Comments

@justinbc820
Copy link

justinbc820 commented Dec 21, 2017

Code

let obj = {
    prop1: function () { },
    prop2: 'hello',
}

let prox = new Proxy(obj, {
    get: function (target, name) {
        return 5;
    }
});

prox.prop1.

Expected behavior:
I would expect that when I type prox.prop1., I would get typescript suggestions for Number.prototype, but instead, I get suggestions for Function.prototype. prox.prop1 will (according to typescript) still be callable as a function, but in runtime, it will clearly be a number and will throw an exception.

Statically evaluate the proxy traps and determine the type of thing being returned to offer proper typescript intellisense.

@justinbc820
Copy link
Author

Here's a screenshot of the issue.

screen shot 2017-12-21 at 10 22 51 am

@ghost
Copy link

ghost commented Dec 21, 2017

You're right, our current definition of Proxy assumes that the output type is the same as the input type, when it could be any type, and just happen to want access to target.
Maybe we could do something like this:

interface ProxeeHandler<T extends object, TOut extends object> {
    get?<K extends keyof TOut>(target: T, p: K, receiver: TOut): TOut[K];
    set?<K extends keyof TOut>(target: T, p: K, value: TOut[K], receiver: TOut): boolean;
}
interface ProxeeConstructor {
    new <T extends object, TOut extends object>(target: T, handler: ProxeeHandler<T, TOut>): TOut;
}
declare var Proxee: ProxeeConstructor;

let obj = {
	prop1: function () { },
	prop2: 'hello',
}

// Type inference can't infer what keys the result is supposed to have.
let prox: Record<keyof typeof obj, number> = new Proxee(obj, {
	get(target, name) {
		return 5;
	}
});

@ghost ghost added the Bug A bug in TypeScript label Dec 21, 2017
@mhegazy mhegazy added Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript Help Wanted You can do this labels Jan 10, 2018
@mhegazy mhegazy added this to the Community milestone Jan 10, 2018
@kapke
Copy link

kapke commented Oct 26, 2018

Out of my head I can tell about 3 cases of Proxy changing interface of a target object:

  1. Adding some methods - for example fantasy-land implementation
  2. Forbiding access to some methods - for example allowing only immutable Array methods
  3. Replacing behavior of particular methods - for example making all Array methods immutable.

So the idea of In and Out types seems reasonable as it covers all of these cases. But to have soundly typed traps it still would require improved return type checking in switch and if statements as this is a valid code now:

interface FooBar {
  foo: number;
  bar: string;
}

function fooOrBar<K extends keyof FooBar>(prop: K): FooBar[K] {
  switch (prop) {
    case "foo": return "foo"; //It should cause a type error
    case "bar": return 5; // And this too
  }
}

@ShanonJackson
Copy link

ShanonJackson commented Dec 25, 2018

I would love proxies to be able to solve what @andy-ms is saying because my use case is where input/output types are different.

I'm desperate because I've wanted Scalas underscore in typescript for a long time, I have the implementation but there's no way to type it here's an example.

I need a way to tell typescript that _.child is actually the converted output type...
(a: T) => T["child"]
and that _.child.children is...
(a: T) => T["child"]["children"]

const buildUnderscore = (f: any) => (path: any[]): any => new Proxy(f, handler(path));
const handler = (path: any[]) => ({
    get: function(obj: any, prop: any, value: any) {
        return buildUnderscore((a: any) => getAtPath(a, path.concat(prop)))(path.concat(prop));
    }
})
/* takes object A and key path ["a", "b", "c"] -> returns A["a"]["b"]["c"] */
const getAtPath = (obj: any, [head, ...tail]: string[]): any => {
    if(head === undefined) return obj;
    return getAtPath(obj[head], tail);
}


/* can be used to short-hand most lambdas now */
const _: any = buildUnderscore((a: any) => a)([]);


const data =  [
        {
            child: {
                name: "bob",
                children: ["sue", "john"]
            }
        },
        {
            child: {
                name: "catness",
                children: ["rue", "hew"]
            }
        },
]

console.log(data.map(_))  // converts to data.map((a) => a)
console.log(data.map(_.child.children[0]))// converts to: data.map((a) => a["child"]["children"][0])
console.log(data.map(_.child))  // converts to data.map((a) => a["child"])

@jerp
Copy link

jerp commented Feb 5, 2020

I stumble upon this. Might be too late... but you could try

    interface MyProxyConstructor {
      new <T, H extends object>(target: T, handler: ProxyHandler<H>): H
    }
    const MyProxy = Proxy as MyProxyConstructor
    let obj = {
      prop1: function() {},
      prop2: 'hello',
    }

    let prox = new MyProxy<typeof obj, { [name: string]: () => number }>(obj, {
      get: function() {
        return 5
      },
    })

    prox.prop1 // type () => number

@tpict
Copy link

tpict commented Aug 20, 2020

Is there any possibility of preserving a generic call signature on the get handler?

interface Demo {
  a: 1 | 2,
  b: 3 | 4,
}

const getter = <T extends unknown>(value: T) => T;

const demo: Demo = {
  a: getter( // cursor here

For this simple example, we get an autocomplete suggestion of 1 | 2 in order to satisfy the Demo interface, even though the argument is typed more loosely as unknown.

It would great to have this behavior when proxies are used to satisfy an interface with stricter typing than that of the get hander (in this case, the same autocomplete suggestion after typing getter[).

@rubenyeghikyan-highway
Copy link

Well its 2022 now. Any updates on this? :)

jahands added a commit to jahands/itty-router that referenced this issue Dec 20, 2022
This was an unintended side effect of moving to script-mode TypeScript.
Unfortunately, TS doesn't handle Proxies very well and thinks that these
are undefined (which is technically true until the proxy is called.)
TypeScript will hopefully make typing proxies better in the future:
microsoft/TypeScript#20846
jahands added a commit to jahands/itty-router that referenced this issue Dec 20, 2022
Optionality here was an unintended side effect of moving to script-mode TypeScript.
Unfortunately, TS doesn't handle Proxies very well and thinks that these
are undefined (which is technically true until the proxy is called.)
TypeScript will hopefully make typing proxies better in the future:
microsoft/TypeScript#20846
@chillcaw
Copy link

chillcaw commented Mar 2, 2023

First of all, any update on this in 2023, and is there any way we can help get something over the line?

Not an update on the issue just a couple of examples of how I've overcome this in the past, in case they can be of use to anyone else!. It's often a case by case task of tricking typescript into calling the proxy!

Bear in mind the proxy examples below still technically require definitions of your keys and what types they return! So it's still not behaving the way a proxy would in vanilla Javascript!

First example:

// typescript needs to know that it can access any string property and that the prop should return our function
interface IFuncHash {
    [element: string]: (t: TemplateStringsArray) => string;
}

class CallableObj extends Function {
    constructor() {
        super();
        return new Proxy(this, {
            apply: (target, prop, args: any[]) => target._call(...args),
            get: (target, prop) => {
                return (input: TemplateStringsArray) => {
                    return input.raw.join(" ");
                };
            },
        });
    }

    _call(...args: any[]) {
        return (args[0] as TemplateStringsArray).raw.join(" ");
    }
}

const db = new CallableObj() as CallableObj & IFuncHash;

console.log(db`my styles here`); // We can call the object no problem!
console.log(db.div`div styles here`); // We can call methods that don't exist in our class via the proxy!
console.log(db.input`input styles here`);

Second Example:

function fakeBaseClass<T>(): new () => Pick<T, keyof T> {
    return class {} as any;
}

// We need to trick TS into thinking our class "contains" the array methods without extending it
class ArrayExt<T> extends fakeBaseClass<Array<any>>() {
    private array: T[];

    constructor(array: T[]) {
        super();
        this.array = array;

        return new Proxy(this, {
            get(target, property) {
                const exists = target[property as keyof typeof target];

                if (exists !== undefined) return exists;

                const underlyingAtt =
                    target.array[property as keyof typeof target.array];

                if (typeof underlyingAtt === "function") {
                    return underlyingAtt.bind(target.array);
                }

                return target.array[property as keyof typeof target.array];
            },
        });
    }

    last(): T {
        return this.array[this.array.length - 1];
    }
}

const array = [1,2,5,1,2,5];
const ext = new ArrayExt(array);
console.log(ext.map((item) => item + 1)); // We can call the underlying Array.prototype.map via our proxy!
console.log(ext.last()); // We can still call members of ArrayExt<T>!

@vantaboard
Copy link

Hey, I would actually really love to have this feature as well. Can we get some feedback on this, please?

@AleksandrHovhannisyan
Copy link

The set trap is even worse than get because it divorces the key and value types from each other, meaning anything goes. I can't for the life of me get the type errors to go away in this simple demo, nor can I actually guarantee that another developer won't ever try to set some illegal key-value combination that violates the constraints of Data:

type Data = {
  aRecord?: Record<string, unknown>,
  aString: string,
  aNumber?: number,
};

const datum: Data = {
  aRecord: { key: "value" },
  aString: "string",
}

const proxy = new Proxy(datum, {
  set(data, key: keyof Data, value) {
    // Type 'any' is not assignable to type 'never'
    data[key] = value;
    return true;
  }
})

https://www.typescriptlang.org/play?#code/C4TwDgpgBAIghsOUC8UDeAoKU4CUIDGA9gE4AmA-AFxT7HkA8AzsCQJYB2A5gDRQCuHANYciAdw4A+HlhwBlVpy40W7bjOxwAcvwC2AIwglqUDnsMkZAXwDcGDMQ4soZBHprxEKdLLyFSZDRoUEIQIDQARABucAA2-BARUFYa8orckapKEdb2js5gJEQAHiDeHBBiUAAKRaUAFK7AenyY2EwQwI0IcHyh4SFhRABmsD18MfEQAJQ+2NhNcADa-QC63pMJdvNQJJ38JBxQrFuyVhhW00A

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript Help Wanted You can do this
Projects
None yet
Development

Successfully merging a pull request may close this issue.