Use TypeScript to describe a Proxy object that changes its property types on assignment

Viewed 169

The Setup

Describing how a proxy object works to the TypeScript compiler has proven a bit challenging. Take for example the case of a Proxy with a setter that transforms any objects in the input to recursively proxy them as well, it would be really great for TypeScript to understand this and allow a direct assignment instead of needing to first coerce the final recursive proxied object. Here's an example, it's also running on TypeScript playground:

interface SimpleObject {
  [index: string]: any
}

type ProxyObject<T> = {
  [K in keyof T]: T[K] extends SimpleObject ? ProxyObject<T[K]> : T[K]
} & {
    isProxy: true
}

function createProxy<T extends SimpleObject>(obj: T): ProxyObject<T> {
  const proxyObject = Object.create(obj, {})
  for (const key in obj) {
    proxyObject[key] = (typeof obj[key] === 'object') ? createProxy(obj[key]) : obj[key]
  }

  return new Proxy(proxyObject, {
    get (...args) {
      // some side effects here perhaps
      console.log(`fetchin value for ${args[2]}`)
      return Reflect.get(...args);
    },
    set (target, property, value) {
        const setTo = (typeof value === 'object' && !value.isProxy) ? createProxy(value) : value
        return Reflect.set(target, property, setTo)
    }
  })
}

const proxied = createProxy({
  text: 'hello world',
  nested: {
    money: 345
  }
})

// This works because it's initially declared as a string
proxied.text = 'Another value'
// This wont work because text is a string, this is good!
// proxied.text = 123

// This will work!
proxied.nested.money = 123

// This works
proxied.nested = createProxy({ money: 555 })

// This will not work because
// > Type '{ money: number; }' is not assignable to type 'ProxyObject<{ money: number; }>'
proxied.nested = { money: 555 }

The Problem

Ideally you there would be a way to inform the TypeScript compiler that the proxied.nested will automatically get converted into the correct type (i.e. ProxyObject<{ money: number}>). You can use a union operator in the ProxyObject definition to allow this:

type ProxyObject<T> = {
  [K in keyof T]: T[K] extends SimpleObject ? ProxyObject<T[K]> | T[K] : T[K]
} & {
    isProxy: true
}

But this doesn't really resolve the issue that proxied.nested changes the type on input, instead all objects in ProxyObject are no longer certain if they are a SimpleObject or a ProxyObject so you have to constantly sniff that out.

The Question: Is there any way to tell TypeScript that this property can be assigned a Union (multiple types) but its value is always a single type when read?

0 Answers
Related