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 causes *ngIf to display blank boxes and what is the solution to resolve this problem?

I am currently working on an HTML project where I need to display objects from an array using Angular. My goal is to only show the objects in the array that are not empty. While I have managed to hide the content of empty objects, the boxes holding this co ...

Can we establish the set values for a function's parameter in advance?

I need to define the available values for a function's parameter in this way: let valueList = [ 'val1', 'val2', 'val3', ]; let getSomething = (parameter: valueList) => { // do something } I want the con ...

Issues with identifying the signature of a class decorator arise when invoked as an expression

I've been following this coding example but I'm running into issues when trying to compile it. Any suggestions on how to troubleshoot this error? import { Component } from '@angular/core'; function log(className) { console.log(class ...

Error: An unexpected character (.) was encountered | Building with npm has failed

When executing "npm run build", I encounter an error with the unexpected token (.) related to object values. Can someone assist me in resolving this issue? I am using tsc build for a react npm library. It seems like there might be a configuration problem ...

Tips for creating an array that aligns with the keys of a type in TypeScript

Currently, I am utilizing the Kysely SQL builder for JS based on Vercel's recommendation, despite the limited documentation and community support. This SQL builder is fully typed, allowing you to create a db object with a schema that recognizes table ...

Adding client-side scripts to a web page in a Node.js environment

Currently, I am embarking on a project involving ts, node, and express. My primary query is whether there exists a method to incorporate typescript files into HTML/ejs that can be executed on the client side (allowing access to document e.t.c., similar to ...

Guide on sending JSON object to Angular custom components

I have implemented a custom element in Angular 7 using the CUSTOM_ELEMENTS_SCHEMA. My app.module.ts code is as follows: export class AppModule { constructor(private injector: Injector) {} ngDoBootstrap() { this.registerCustomElements( ...

Troubles encountered when trying to execute mocha within Firebase functions

My latest project involved developing a Node/Typescript app that interacted with data from Firebase Cloud Firestore. The app performed flawlessly, and I conducted endpoint testing using simple mocha commands on the generated .js file. Below is an example o ...

What exactly does "nothing" mean in Node when using async await?

I have a method as shown below: private async sendToAll(clients) { for(const client of clients) { this.send(client, message); await true; // What should I put here to allow the rest of the application to continue executi ...

Managing null values in RxJS map function

I'm facing a scenario where my Angular service retrieves values from an HTTP GET request and maps them to an Observable object of a specific type. Sometimes, one of the properties has a string value, while other times it's null, which I want to d ...

Updating a one-to-one relationship in TypeORM with Node.js and TypeScript can be achieved by following these steps

I am working with two entities, one is called Filter and the other is Dataset. They have a one-to-one relationship. I need help in updating the Filter entity based on Dataset using Repository with promises. The code is written in a file named node.ts. Th ...

Printing error stack that includes the source from the source map

I've been trying to take advantage of the native support for source maps in Node, but I'm having trouble getting them to work when printing errors to the console. Despite running node with --enable-source-maps and using the source-map-support pa ...

Generating an array of elements from a massive disorganized object

I am facing a challenge in TypeScript where I need to convert poorly formatted data from an API into an array of objects. The data is currently structured as one large object, which poses a problem. Here is a snippet of the data: Your data here... The go ...

Is there a simple method to eliminate devDependencies from the ultimate package using esbuild?

My current challenge involves using esbuild to package my lambda functions. However, during the build generation for deployment, I encounter an alert indicating that the package size exceeds the limit, as shown in the image below. File too large In explo ...

Steps for specifying the required type of an Object literal in Typescript

Let's analyze a straightforward code snippet interface Foo{ bar:string; idx:number; } const test1:Foo={bar:'name'}; // this is highly recommended as it includes all required fields const test2={bar:'name'} as Foo; // this is ...

Contrast between employing typeof for a type parameter in a generic function and not using it

Can you explain the difference between using InstanceType<typeof UserManager> and InstanceType<UserManager> I'm trying to understand TypeScript better. I noticed in TS' typeof documentation that SomeGeneric<typeof UserManager> ...

Using Typescript with AWS Lambda can sometimes be a bit tricky. For example, when trying to invoke your Lambda function locally using "sam local invoke", you might encounter an error stating

Attempting to deploy an AWS Lambda function using the sam command with AWS's Hello World Example Typescript template, but encountering issues with the example template. Suspecting a bug within AWS causing this problem. This issue can be easily repli ...

The Type {children: Element; } is distinct and does not share any properties with type IntrinsicAttributes

I am encountering an issue in my React application where I am unable to nest components within other components. The error is occurring in both the Header component and the Search component. Specifically, I am receiving the following error in the Header co ...

Unable to load the default value for ion-select in TypeScript

I have reviewed this question, this question, and this question. However, none of them seem to provide a straightforward solution for what I am searching for. <ion-list> <ion-item> <ion-label>Select Book</ion-label> <i ...

Removing the AM and PM from OwlDateTime in Angular is simple since the time format is already in 24-hour time

Using OwlDateTime in a 24-hour format: <div *ngIf="isSchedule" class="form-inline"> <label style='margin-right:5px ;margin-left:210px'> Date Time: <input [owlDateTimeTrigger]="dt" [owlDateTime]="dt" class="form-control" placeh ...