What is the reason for the failure of the "keyof" method on this specific generic type within a Proxy object created by a class constructor?

I'm encountering difficulties when utilizing a generic type in combination with keyof inside a Proxy():

The following example code is not functioning and indicates a lack of assignable types:

interface SomeDataStructure {
    name?: string;
}

class DataWrapper<T extends SomeDataStructure> {
    data: T;
    
    constructor(data: T) {
        this.data = data;

        return new Proxy(this, {
            get: (target: this, property: keyof T): any => {
                return target.data[property];
            }
        });
    }
}

The error message (on get):

Type '(target: this, property: keyof T) => any' is not compatible with type '(target: this, p: 
string | symbol, receiver: any) => any'.
  Parameters 'property' and 'p' have incompatible types.
    Type 'string | symbol' cannot be assigned to type 'keyof T'.
      Type 'string' cannot be assigned to type 'keyof T'.
        Type 'string' cannot be assigned to type '"name"'.ts(2322)

If I directly use keyof SomeDataStructure, instead of keyof T, the error vanishes and everything seems to work fine:

interface SomeDataStructure {
    name?: string;
}

class DataWrapper<T extends SomeDataStructure> {
    data: T;
    
    constructor(data: T) {
        this.data = data;

        return new Proxy(this, {
            get: (target: this, property: keyof SomeDataStructure): any => {
                return target.data[property];
            }
        });
    }
}

Alternatively, specifying

keyof T extends SomeDataStructure
(similar to the class definition) also resolves the issue. The usage of keyof T in general for class methods outside of the Proxy() functions smoothly.

It appears that the type of T gets lost when wrapping things within new Proxy(). How can I maintain the reference to T for keyof T in this scenario?

Answer №1

One should not assume that the property is always of type keyof T, as TypeScript tends to allow get handlers to make risky assumptions about the property. The error occurs because keyof T poses an unsafe condition that confuses the compiler.


To begin with, it is not safe to presume that property belongs to the type keyof T.

The generic type parameter T is restricted to SomeDataStructure, which implies it could potentially be a subtype of SomeDataStructure containing additional properties unknown to SomeDataStructure:

const dw = new DataWrapper({
  name: "abc",
  otherProp: 123,
  4: true,
}); // compiles okay

Moreover, the provided data might be a subtype of T with properties not defined within T:

interface Foo extends SomeDataStructure {
  name: string
  otherProp: number,
  4: boolean
}
const bar = { name: "abc", otherProp: 123, 4: true, yetAnotherProp: "hello" };
const foo: Foo = bar; // okay
const dw2 = new DataWrapper(foo);
// const dw2: DataWrapper<Foo>

In this scenario, dw2 is considered to be of type DataWrapper<Foo>, despite the actual data containing a property named

yetAnotherProp</code which is absent in <code>Foo
. This aligns with how structural typing operates in TypeScript, and it's by design.


Hence, your proxy handler should be able to handle any property key, rather than just assuming it's keyof T. According to the TypeScript ProxyHandler typings for get, the property parameter is expected to be of type string | symbol.

However, due to its declaration using method syntax ({ get(⋯): ⋯ }) instead of function property syntax ({ get: (⋯) => ⋯ }), TypeScript treats it as being bivariant in its parameter types, accommodating both safer and riskier assumptions regarding the type of property.

This manifests as an error when using keyof T since it neither widens nor narrows to string | symbol, making it ineligible according to the current TypeScript setup.


To resolve this issue, it is advised to develop code capable of handling any string | symbol property effortlessly. If you are unconcerned about extra keys, consider Exclude-ing number from keyof T to provide a narrower interpretation resulting in proper bivariant behavior:

interface SomeDataStructure {
  name?: string;
}

class DataWrapper<T extends SomeDataStructure> {
  data: T;

  constructor(data: T) {
    this.data = data;

    return new Proxy(this, {
      get: (target: this, property: Exclude<keyof T, number>): any => { // okay
        return target.data[property];
      }
    });
  }
}

This resolves the issue effectively.

Playground link to code

Answer №2

The error message you're receiving is valid: TypeScript doesn't consider a string | symbol to be compatible with keyof T. These are indeed two distinct types. ("Compatible" means that string | symbol must be the same as or narrower than keyof T - which is not the case.)

The issue of not being able to specify to TypeScript that it should always anticipate a keyof T to be provided is due to what I believe is a poorly defined type for ProxyHandler. You can see this by accessing the definition of the get function in your editor.

Currently, the typing is:

get?(target: T, p: string | symbol, receiver: any): any;

What you actually want is:

get?<K = keyof T>(target: T, p: K, receiver: any): T[K];

If you had the second typing, you could interact with TypeScript more accurately:

get: <K extends keyof T>(target: this, property: K): T[K] => {
  const value = target.data[ property ];
  return value; // correctly resolves to `T[K]`
}

You can try this out in your editor by modifying your lib.es20XX.proxy.d.ts file and inserting the alternative typing there:

    /**
     * A trap for getting a property value.
     * @param target The original object being proxied.
     * @param p The name or `Symbol` of the property to retrieve.
     * @param receiver The proxy or an object extending from the proxy.
     */
    get?(target: T, p: string | symbol, receiver: any): any;
    get?<K = keyof T>(target: T, p: K, receiver: any): T[K];

I'm afraid my expertise on this particular topic is limited to this point. I'm uncertain if there's a quick fix that would allow you to refine the default typings without introducing your own .d.ts files. Perhaps someone else can provide more actionable guidance.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to iterate through all class properties that are specified using class-validator?

I have a class defined using the class-validator package. class Shape { @IsString() value?: string @IsString() id?: string } I am trying to find a way to retrieve the properties and types specified in this class. Is there a method to do s ...

The evaluation of mongodb-memory-server is experiencing issues with either failing or timing out

While setting up mongodb-memory-server in my backend for testing purposes, I encountered some issues during test execution that require debugging. The problem arises when running a test that creates a MongoDB document within the service being tested, leadi ...

Show all span elements in a map except for the last one

Within my ReactJS application, I have implemented a mapping function to iterate through an Object. In between each element generated from the mapping process, I am including a span containing a simple care symbol. The following code snippet demonstrates t ...

A method for modifying the key within a nested array object and then outputting the updated array object

Suppose I have an array called arr1 and an object named arr2 containing a nested array called config. If the key in the object from arr1 matches with an id within the nested config and further within the questions array, then replace that key (in the arr1 ...

Is it possible to import the identical file twice consecutively using html and typescript?

I encountered an issue with an input element in my HTML file. Here's what it looks like: <input type="file" (change)="receiveFile($event)" id="inputFileButton" hidden /> This input element is designed for users to import files. Wh ...

Dynamically attach rows to a table in Angular by triggering a TypeScript method with a button click

I need help creating a button that will add rows to a table dynamically when pressed. However, I am encountering an error when trying to call the function in TypeScript (save_row()). How can I successfully call the function in TypeScript and dynamically a ...

Asynchronous NestJs HTTP service request

Is there a way to implement Async/Await on the HttpService in NestJs? The code snippet below does not seem to be functioning as expected: async create(data) { return await this.httpService.post(url, data); } ...

Injecting a component in Angular 2 using an HTML selector

When I tried to access a component created using a selector within some HTML, I misunderstood the hierarchical provider creation process. I thought providers would look for an existing instance and provide that when injected into another component. In my ...

What could be the reason for the tsc command not displaying compilation errors when compiling a particular file?

My file, titled app.ts, contains the following code snippet: interface Foo { bar:String; } const fn = (foo? :Foo) => foo.bar; When I run tsc from the root folder with strict:true in my tsconfig.json file, I receive an error message like this ...

Oops! Looks like there's an issue with the type error: value.forEach is

I am working on creating an update form in Angular 6 using FormArray. Below is the code snippet I have in editfrom.TS : // Initialising FormArray valueIngrident = new FormArray([]); constructor(private brandService: BrandService, private PValueInfoSe ...

How to use Angular template syntax to assign an async array to multiple variables

When working in JS, there is a clever method for assigning values from an array to new variables with ease: let [a, b, c] = [1, 2, 3]; // a = 1, b = 2, c = 3 I started thinking about whether I could achieve a similar elegant solution using Angular's ...

Using arrays as props in React with Typescript

In my code, I have a Navbar component that contains a NavDropdown component. I want to pass an array as a prop to the NavDropdown component in order to display its dropdown items. The array will be structured like this: DropDownItems: [ { ...

What is the best way to enable code sharing between two TypeScript projects housed within the same repository?

Our project has the following structure: api (dir) -- package.json -- tsconfig.json -- src (dir) -- client (dir) ---- package.json ---- tsconfig.json ---- src (dir) The "client" directory houses a create-react-app project that proxies to the API d ...

Utilizing union type return values in Typescript

Looking to incorporate shelljs (via DefinitelyTyped) into my Typescript 1.5-beta project. I want to utilize the exec function with the specified signature: export function exec(command: string, options: ExecOptions): ExecOutputReturnValue | child.ChildPro ...

What is the method for utilizing string interpolation in Angular/Typescript in order to retrieve a value from a variable?

I have a variable called demoVars, which is an array of objects with properties var1, var2, and var3. In my component class, I have a variable named selectedVar that holds the name of one of these properties: var1, var2, or var3. I want to dynamically pu ...

Is it possible to create cloud functions for Firebase using both JavaScript and TypeScript?

For my Firebase project, I have successfully deployed around 4 or 5 functions using JavaScript. However, I now wish to incorporate async-await into 2 of these functions. As such, I am considering converting these specific functions to TypeScript. My conc ...

Error: global not declared in the context of web3

I've been attempting to integrate Web3 into my Ionic v4 project for some time now. However, I keep encountering errors when I try to serve the project. Specifically, I receive an error message stating that Reference Error: global is not defined. Cre ...

During the compilation process, Angular could not locate the exported enum

In the file models.ts, I have defined the following enum: export enum REPORTTYPE { CUSTOMER, EMPLOYEE, PROJECT } After defining it, I use this enum inside another class like so: console.log(REPORTTYPE.CUSTOMER); When I save the file, the IDE automati ...

Typescript Declarations for OpenLayers version 6

The package @types/openlayers found at https://www.npmjs.com/package/@types/openlayers only provides type definitions for version 4.6 of OpenLayers. This is clearly stated in the top comment within the file index.d.ts. If types for OpenLayers 6 are not av ...

When a ListView item is clicked, a label will display text with text wrapping specific to the selected item in the list

Within the listview items, there is a label that should expand when clicked. For example, initially it only shows one line of text. Upon clicking on the label, it should expand to show 10 lines of text. Current Issue: At present, when I click on the firs ...