Omer Gronich
Omer Gronich

Reputation: 182

TypeScript Partials: How to map specific fields from an object to another object

I'm working on a feature that retrieves a list of products from some E-commerce API. I'm trying to add an ability to request specific fields from the products, removing the unnecessary fields.

This is the code:

    interface Product {
     images: string[],
     title: string;
     id: number;
     currency: string;
     price: number;
     created: string | Date;
     description: string; 
    }
    
    const getProducts = (selectedProperties: (keyof Product)[]) => {

      // imagine this is a call to an API to get a list of products 
      const products: Product[] = [
                        {
                            id: 1,
                            images: [],
                            title: 'a shirt',
                            currency: "USD",
                            price: 10,
                            created: "2021-04-29T11:21:53.386Z",
                            description: "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
                         }
                     ];

    if(selectedProperties?.length){
        return products.map(product => {
            const result: Partial<Product> = {};

            selectedProperties.forEach(prop => {
                result[prop] = product[prop]; // <--- This line returns the error: Type 'string | number | string[] | Date' is not assignable to type 'undefined'. Type 'string' is not assignable to type 'undefined'
            })

            return result;
        })
    }

    return products;
}

Here is a link to a code sandbox with the error so you could see it for yourself: link (look at line 30)

What am I doing wrong here exactly that is causing the TypeScript error?

Upvotes: 6

Views: 2038

Answers (1)

jcalz
jcalz

Reputation: 329943

The problem here is that the compiler infers the type of prop to be keyof Product, which is a wide type corresponding to multiple possible strings. And while you understand that result[prop] = product[prop] should be fine because both refer to the same exact value named prop, the compiler only really sees the types of these things. It can't see the difference between that and result[prop2] = product[prop1] where prop2 and prop1 are both keyof T. You'd agree that such a line is a mistake unless you can constrain prop1 and prop2 to the very same literal key type.

This is a pain point in TypeScript; there is some discussion in microsoft/TypeScript#30769, the change made for TypeScript 3.5 responsible for this checking... it improved soundness, but at the expense of adding some false positives like this. The specific problem with copying properties is an open issue at microsoft/TypeScript#32693. This comment implies that the TS team is aware of the issue and think something should be done to support copying properties from one object to another. But who knows when or if this will actually happen. If you care you might want to go there and give it a 👍, but I doubt that will have much of an impact.


For now the way to proceed is probably to make the callback generic in K extends keyof Product and have prop be of type K:

selectedProperties.forEach(<K extends keyof Product>(prop: K) => {
  result[prop] = product[prop];  // no error
})

This makes the error go away. Technically this has the same problem as before, since nothing stops K from being the full union keyof Product, but the compiler explicitly allows assignments from Product[K] to Product[K] for generic K, despite such potential unsoundness.

Playground link to code

Upvotes: 4

Related Questions