#why is propertyKey parameter in `ProxyHandler` not of type `PropertyKey`

77 messages · Page 1 of 1 (latest)

brave token
#

In src/lib/es2015.proxy.d.ts methods that have a property key parameter expect type string | symbol rather than PropertyKey. Such as get?(target: T, p: string | symbol, receiver: any): any;.

The corresponding methods in src/lib/es2015.reflect.d.ts, however, have property key parameters that have type PropertyKey.

Is there a reason why number is restricted in proxy handlers but not in reflect?

In Typescript 3, the property key parameter had type PropertyKey in ProxyHandler. It was in Typescript 4+ that it was changed to string | symbol

brazen night
#

Well keys are all string | symbol

#

number is inbound only. When you assign a property with a numeric key, it gets converted to a string

proper kilnBOT
#
sandiford#0

Preview:ts const o = { 1: 1, } for (const k in o) { console.log(typeof k) }

brazen night
#

Generally string | number | symbol is the correct type when assigning (or both), and string | symbol is correct when reading or receiving keys.

#

@brave token

brave token
#

By not typing it as PropertyKey, it means any proxied object is more restrictive than the original. Since the original object can still accept numbers that are implicitly converted to strings.

brazen night
#

Can you show an example of where it is a problem?

#

The type here is for the get function in the handler. I don't see how that restricts anything.

brave token
#

Yes, let me try to clean up the example.

brave token
#

Ok, here's what I was playing around with:

const record: Record<string, number[]> = {};
const defaultListRecordProxy = new Proxy(numberListRecord, {
  get: function (target: Record<string, number[]>, key: string): number[] {
    if (!(key in target)) {
      Reflect.set(target, key, []);
    }

    return Reflect.get(target, key);
  },
});

defaultListRecordProxy["a"].push(1);
#

Switch out property string with number and get will complain of type error.

brazen night
#

where?

#

A TS playground that shows the error is easiest for me

brave token
#

Ok, sharing in the playground

brazen night
#

You will have to send the link

brave token
#

ohh sorry

brazen night
#

The link encodes what you write live, so whoever clicks the link sees exactly what was there when you copied it

brave token
#

the link is very long? is there a way to shorten the link?

brazen night
brave token
brazen night
#

Oh that's not long

#

Try pasting the whole link here and see what happens

brave token
#

i used the shortener

brazen night
#

I know. Try the full link

proper kilnBOT
#
rtvu87#0

Preview:```ts
const record: Record<string, number[]> = {}
const defaultListRecordProxy = new Proxy(record, {
get: function (
target: Record<string, number[]>,
key: string
): number[] {
if (!(key in target)) {
Reflect.set(target, key, [])
}

return Reflect.get(target, key

...```

brazen night
#

Sometimes the link gets so long that discord won't let you send it. But no problem here

brave token
#

It was because i didn't delete the sample code... sorry

brazen night
#

So as I said earlier, objects can't have numeric keys

#
function (target: Record<number, number[]>, key: number)

this code doesn't make sense because the keys of Record<number, number[]> are strings

brave token
#

But you can have type Record<number, number>. Yes, the number is being converted to a string property, the type checker will enforce using numbers at the property value on the object.

brazen night
#

The proxy get function will give you a string

brave token
#

Yes, currently how proxy is typed forces to use string or symbol.

brazen night
#

The type you give you an argument does not change it's behaviour

#

Types are only uses for checking at compile time. They are deleted and don't affect program behaviour.

#

the get function gives strings or symbols.

#

To be more specific the get function for Record<number, number> will give you keys of type ${number} , which is a string representation of a number

#

I can't write it haha

proper kilnBOT
#
sandiford#0

Preview:ts ... get: function ( target: Record<number, number[]>, key: `${number}` ): number[ ...

brave token
#

Maybe to clarify, I'm asking why did it go from string | number | symbol to string | symbol from typescript 3 to 4.

brazen night
#

Because it was incorrect before I expect. The function returns a string or symbol, why should it give an incorrect statement?

proper kilnBOT
#
sandiford#0

Preview:```ts
const record: Record<string, number[]> = {}
const defaultListRecordProxy = new Proxy(record, {
get: function (
target: Record<string, number[]>,
key: string
): number[] {
if (!(key in target)) {
Reflect.set(target, key, [])
}

return Reflect.get(target, key

...```

brazen night
#

Here is an example of where we type the key as : number and cause a runtime error

#

It's really simple, we don't call a value a number if it is not a number. Surely there is the start of any sensible typing?

#

It's a bit of an awkward error due to how JS does those implicit conversions, but only 1 way. But that wouldn't help matters.

brave token
#

Huh... then why is there the standard type PropertyKey? I thought implied its standard to treat numbers as keys.

#

Similar to how arrays are indexed with number keys. Internally, they are objects using string keys no?

brazen night
#

Internally they are strings yes

#

PropertyKey is arguably poorly named. If you want one type to cover all PropertyKeys then it worked, but strictly it should be string | symbol, and number should be allowed in certain places where it will be converted

#

Basically JS is a bit wonky here, and that makes typing it awkward. Would be much simpler if we couldn't use numbers

brave token
#

Got it. New to typescript, so didn't know to avoid number keys.

#

So should use Map if i want number keys right?

brazen night
#

I wouldn't avoid them... just, sometimes you have to do awkward things

#

You can use map if you want true number keys I think

#

I'm certainly not saying not to use number records or number indexers on objects

#

But in your proxy function you would just handle the key as a string. Sometimes you might* have to use Number(key) or key as number or key as typeof MyRecord

proper kilnBOT
#
sandiford#0

Preview:ts const record1: Record<number, number[]> = {} const defaultListRecordProxy1 = new Proxy(record1, { get: function ( target: Record<number, number[]>, key: `${number}` ): number[] { if (!(key in target)) { Reflect.set(target, key, []) } return Reflect.get(target, ke ...

brazen night
#

All of these methods work

#

You don't need to use Reflect by the way.

#

The typing is a bit strange on that. Seems to let you enter keys that don't match the object type, and then return any

#

rather than failing.

brave token
#

Thanks, still playing around with the idea. Wanted to see if I can implement something similar to python's defaultdict.

brazen night
#

I expect so

#

What does it do that you can't do with a Map?

brave token
#

defaultdict is initialized with a generator for a default value

#

so when you access a property that hasn't been defined, it returns a default value

brazen night
#

Ah

#

seems like you already have it?

#

With your implementation

brave token
#

Wanted to generalize it. Will need to wrap it in a factory function.

brazen night
#

Might be able to do something with a generic and a conditional

#

I don't actually understand the problem you're having, but there's problem a solution.

brave token
#
function createDefaultRecord<T>(defaultFactory: () => T) {
  const record: Record<string, T> = {};
  return new Proxy(record, {
    get: function (target: Record<string, T>, key: string): T {
      if (!Reflect.has(target, key)) {
        Reflect.set(target, key, defaultFactory());
      }

      return Reflect.get(target, key);
    },
  });
}