When trying to access a specific property of an object in Typescript using a key that is defined as a subset of

UPDATE: Take a look at the revised solution below, inspired by @GarlefWegart's input.

I've been exploring the creation of generic typings for dynamic GraphQL query outcomes (mostly for fun, as I suspect similar solutions already exist).

I'm on the right track, but encountering a peculiar issue. You can find the complete code here in a testing environment, and replicated below.

The issue arises from attempting to index an object utilizing the keys of a derived object, which should work but is failing for unknown reasons. To clarify in the Result declaration, I am unable to index T using K, despite having K defined as a key of U, and U being recognized as a subset of the properties of T. This implies that all keys of U are inherently also keys of T, making it supposedly safe to index T with any key from U. Yet, Typescript rejects this approach.

type SimpleValue = null | string | number | boolean;
type SimpleObject =  { [k: string]: SimpleValue | SimpleObject | Array<SimpleValue> | Array<SimpleObject> };

type Projection<T extends SimpleObject> = {
    [K in keyof T]?:
        T[K] extends SimpleObject
            ? Projection<T[K]>
            : T[K] extends Array<infer A>
                ? A extends SimpleObject
                    ? Projection<A>
                    : boolean
                : boolean;
};

type Result<T extends SimpleObject, U extends Projection<T>> = {
    [K in keyof U]:
        U[K] extends false
            ? never                            // exclude values for false keys
            : U[K] extends true
                ? T[K]                         // keep the original type for true keys
               // ^^vv All references to T[K] trigger errors
                : T[K] extends Array<infer A>
                    ? Array<Result<A, U[K]>>   // Deliver an array of projection results when dealing with an array originally
                    : Result<T[K], U[K]>;      // Otherwise treat it as an object and return the respective projection result
}


type User = {
  id: string;
  email: string;
  approved: string;
  address: {
    street1: string;
    city: string;
    state: string;
    country: {
      code: string;
      allowed: boolean;
    }
  };
  docs: Array<{
    id: string;
    url: string;
    approved: boolean;
  }>
}

const projection: Projection<User> = {
  id: false,
  email: true,
  address: {
    country: {
      code: true
    }
  },
  docs: {
    id: true,
    url: true
  }
}

const result: Result<User, typeof projection> = {
    email: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2a474f6a5f5904494547">[email protected]</a>",
    address: {
        country: {
            code: "US"
        }
    },
    docs: [
        {
            id: "1",
            url: "https://abcde.com/docs/1"
        },
        {
            id: "2",
            url: "https://abcde.com/docs/2"
        }
    ]
}

Your feedback and insights are welcomed.

Revision Mar 10, 2021

Following Garlef Wegart's suggestion provided earlier, a satisfactory resolution has been achieved. The updated code can be viewed here.

Kindly note, however, that it's quite delicate. It suits my specific requirements well since I'm structuring types for a GraphQL API where responses arrive as unknown and are then cast based on input parameters. These types might not be universally applicable. For me, the focal point was not the assignment aspect but rather the utilization of the resulting type, which this solution adequately addresses. Good luck to others tackling similar challenges!

Additional Info: I have also made these types available as a compact Typescript package on GitHub (here). To use it, simply append

@kael-shipman:repository=https://npm.pkg.github.com/kael-shipman
to your npmrc file and proceed with installing the package as usual.

Answer №1

It's undeniably evident, as per the definition of Projection, that keyof Projection<T> forms a subset of keyof T for a specific T.

HOWEVER: If U extends(!) Projection<T>, then U itself can contain keys not found in T. Furthermore, the values associated with these keys can vary widely.

Hence, traversing over keyof U isn't the correct approach to take. Instead, one should iterate over keyof T & keyof U.

Additionally, it is advisable to further refine U to only include desired properties by incorporating

U extends ... & Record<string, DesiredConstraints>
. Neglecting this restriction could allow the passing of objects with undesirable properties. (Perhaps, in your scenario, it should be ... & SimpleObject?)

Here's a simplified illustration (sans the intricacies of your domain) showcasing some of the nuances (Playground link):

type Subobject<T> = {
  [k in keyof T as T[k] extends "pass" ? k : never]:
    T[k]
}

type SomeGeneric<T, U extends Subobject<T>> = {
  [k in keyof U]:
    k extends keyof T
      ? "yep"
      : "nope"
}

type Sub = Subobject<{ a: 1, b: "pass" }>

// `c: "nope"` should not be included in our result!
type NotWanted = SomeGeneric<{ a: 1, b: "pass" }, { b: "pass", c: "other" }>


type Safe<T, U extends Subobject<T>> = {
  [k in (keyof U & keyof T)]:
    k extends keyof T
      ? "yep"
      : "nope"
}

type Yeah = Safe<{ a: 1, b: "pass" }, { b: "pass", c: "other" }>

// Another issue: U might have unwanted properties!
function doStuffWithU<T, U extends Subobject<T>>(u: U) {
  for (const val of Object.values(u)) {
    if (val === "other") {
      throw Error('Our code breaks if it receives "other"')
    }
  }
}

const u = { b: "pass", c: "other" } as const
// This will break but the compiler does not complain!
const error = doStuffWithU<Sub, typeof u>(u)


// A solution to prevent this
type SafeAndOnlyAllowedProperties<T, U extends Subobject<T> & Record<string, "pass">> = {
  [k in (keyof U & keyof T)]:
    // No errors here due to `& Record<string, "pass">`
    OnlyAcceptsPass<U[k]>
}

type OnlyAcceptsPass<V extends "pass"> = "pass!"

// The type checker now detects that "other" cannot be assigned to type "pass"
type HellYeah = SafeAndOnlyAllowedProperties<{ a: 1, b: "pass" }, { b: "pass", c: "other" }>

EDIT: Upon reflection, when defining a function instead of a generic type, you can also utilize the following pattern to prevent incorrect inputs

const safeFn = <U extends Allowed>(u: U & Constraint) => {
  // ...
}

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

Scroll to the top on every Angular 5 route change

Currently, I am utilizing Angular 5 for my project. Within the dashboard interface, there are various sections with varying amounts of content. Some sections contain only a small amount of information, while others have large amounts of content. However, w ...

Tips for inserting an HTML element within an exported constant

I need help formatting an email hyperlink within a big block of text. Here is the code snippet: const myEmail = '<a href="mailto:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2e4b564f435e424b6e4b564f435e424b004d41 ...

obtaining the value of an input using typescript (put request)

Does anyone know how to extract input values and store them as JSON? I'm having trouble with accessing the input value in this scenario. When I attempt document.querySelector("todo-text").value, it results in an error. const NewTodo: React.FC<NewT ...

Convert parameterized lambdas for success and failure into an observable using RxJS

There is a function exported by a library that I am currently using: export function read( urlOrRequest: any, success?: (data: any, response: any) => void, error?: (error: Object) => void, handler?: Handler, httpClient?: Object, ...

What is the best way to incorporate an external .css file into my Angular project by referencing its URL?

I'm managing a collection of CSS files online and I need to incorporate each one into my project based on its specific requirements. One component in particular is connected to different numerical IDs in the router. I am looking for a way to dynamica ...

Why use rxjs observables if they don't respond to updates?

I have an array of items that I turn into an observable using the of function. I create the observable before populating the array. However, when the array is finally populated, the callback provided to subscribe does not execute. As far as I know, th ...

Executing one controller function from another controller in Typescript

There are two typescript controllers in my project Controller A { public methodOfA() {//perform some action} } Controller B { public methodOfB() {//perform some action} } I am trying to implement the following functionality Controller B { ...

Vue.js 3 with TypeScript is throwing an error: "Module 'xxxxxx' cannot be located, or its corresponding type declarations are missing."

I developed a pagination plugin using Vue JS 2, but encountered an error when trying to integrate it into a project that uses Vue 3 with TypeScript. The error message displayed is 'Cannot find module 'l-pagination' or its corresponding type ...

How to dynamically load a component within a class-based Vue component

I am facing an issue with loading two components dynamically using an object map. Info (options-based) SearchBar (class-based) While it works for the options-based component, I encounter an error stating _currentTab is undefined when trying to load a si ...

Encountering Compilation Issues Post Upgrading to Angular 9

I recently upgraded my Angular application from version 8 to version 9, following the official guide. However, after the upgrade, I encountered errors that prevent my application from building. The specific errors include: "Module not found: Error: Can ...

What is the best way to automatically log out a user when a different user logs in on the same browser?

Currently, I am encountering an issue where there are two separate dashboards for different types of users - one for admin and the other for a merchant. The problem arises when an admin logs in on one tab and then a merchant logs in on another tab in the s ...

Firebase Angular encountering issues with AngularFirestoreModule

I have encountered a challenge with Firebase authentication in my Angular applications. Due to updated read and write rules that require auth!=null, I successfully implemented Firebase authentication in one of my apps using Angular 13. Now, I am trying to ...

Required attributes not found for data type in TypeScript

When the following code snippet is executed: @Mutation remove_bought_products(productsToBeRemoved: Array<I.Product>) { const tmpProductsInVendingMachine: Array<I.Product> = Object.values(this.productsInVendingMachine); const reducedPro ...

Suggestions for efficiently filtering nested objects with multiple levels in RXJS within an Angular environment?

Just a Quick Query: Excuse me, I am new to Typescipt & RxJS. I have this JSON data: [ { "ID": "", "UEN": "", "Name": "", "Address": "", "Telephone&quo ...

Error: Property 'content' is not defined and cannot be read

I encountered an issue with a config file while attempting to build with AOT using the command ionic cordova build android --prod Error: ./src/config/.env.ts Module build failed: TypeError: Cannot read property 'content' of undefined at Object ...

What are the drawbacks of combining exports through re-exporting in TypeScript?

Lately in TypeScript discussions, there seems to be a negative viewpoint on namespace BAD. However, I see value in organizing related declarations within a single namespace, similar to a library, to avoid excessive import statements. I have come across th ...

Is there a way to implement retry functionality with a delay in RxJs without resorting to the outdated retryWhen method?

I'd like to implement a retry mechanism for an observable chain with a delay of 2 seconds. While researching, I found some solutions using retryWhen. However, it appears that retryWhen is deprecated and I prefer not to use it. The retry with delay s ...

Methods for retrieving a single document ID from a Firebase collection using Angular

Currently, I am utilizing Angular 11 in conjunction with Firebase Firestore for my project. My objective is to retrieve the unique document id from a single document within my collection. This will enable me to establish a sub-collection named "schedules" ...

Attempting to locate a method to update information post-editing or deletion in angular

Are there any methods similar to notifyDataSetChange() in Android Studio, or functions with similar capabilities? ...

What is the reason for `downlevelIteration` not being enabled by default?

When directing towards ES5 and using the spread operator ... to convert an Iterator to an Array, it prompts the need for the -downlevelIteration compiler option. Enabling this option allows the spread operators to function without any errors. I'm cur ...