In Typescript, a type that is a union of object keys cannot be utilized as a key within the object

One of the challenges I am facing involves a function that takes an object as a parameter and returns another object, with one of its properties being the value of a key in the original object. When attempting to retrieve this value using obj[key] notation, I encounter an issue.

I understand that the key must be of the proper type in order to use it like this. I cannot simply use a string as the key if the interface does not have [key: string]: any. However, my key is a union of strings that were keys in the passed object, such as 'user' or 'name'.

interface Twitter<T> {
  name: T;
  user: T;
  sex: T extends string ? boolean : string;
}
interface Facebook<T> {
  appName: T;
  whatever: T;
  imjustanexample: T;
}

type TwFbObjKeys = keyof Twitter<string> | keyof Facebook<string>

The function in question looks like this:

public static getArrayOfObjects(
        queryParameters: Twitter<string> | Facebook<string>,
    ) {
        const keys = Object.keys(queryParameters) as TwFbObjKeys[];
        return keys.map((paramId) => ({
            paramId,
            value: queryParameters[paramId],
        }));
    }

I expected that using paramId, which is of type 'name' | 'sex' | 'appName' | ..., as a key for an object containing these keys would not result in an error. Unfortunately, I receive the following error:

TS7053: Element implicitly has an 'any' type because expression of type 'name' | 'sex' | 'appName' | ... can't be used to index type Twitter | Facebook. Property 'name' does not exist on type Twitter | Facebook

This issue has consumed several hours of my time. Any suggestions on how to resolve it?

Answer №1

To optimize the function parameter queryParameters, it is recommended to define it as a generic type T with the constraint

<T extends Twitter<string> | Facebook<string>>
rather than a union type
Twitter<string> | Facebook<string>
:

function getArrayOfObjects<T extends Twitter<string> | Facebook<string>>(
    queryParameters: T
) {
    const keys = Object.keys(queryParameters) as (keyof T)[];
    return keys.map((paramId) => ({
        paramId,
        value: queryParameters[paramId],
    }));
}

Playground

Explanation:

Using generics ensures that the type of the argument passed in for queryParameters will be maintained, while also guaranteeing that it will be a subtype of

Twitter<string> | Facebook<string>
. The use of paramId with type keyof T allows access to the properties of queryParameters.

If a union type

Twitter<string> | Facebook<string>
was used, queryParameters would remain undetermined within the function and could potentially still be either Twitter<string> or Facebook<string>, leading to issues with the keyof operator and property access. This limitation arises from the lack of common properties between Twitter<string> and Facebook<string>.

To highlight this issue further, consider attempting to define TwFbObjKeys as

// "name" | "user" | "sex" | "appName" | "whatever" | "imjustanexapmle"
type TwFbObjKeys = keyof Twitter<string> | keyof Facebook<string>

// not this: keyof only applies to Twitter<string> here: "name" | "user" | "sex" | Facebook<string>
type TwFbObjKeys_not = keyof Twitter<string> | Facebook<string>

However, merely defining TwFbObjKeys alone does not resolve the underlying issue. For instance:

declare const queryParameters: Twitter<string> | Facebook<string>

// type keysOfQueryParameters = never
type keysOfQueryParameters = keyof typeof queryParameters

// error: doesn't work either
queryParameters[unionKeys]

// this would work, if both Twitter and Facebook have "common" prop
queryParameters["common"]

Playground

By utilizing a generic type, we are able to safely access the keys of the passed-in queryParameters.

Answer №2

It seems like the issue lies within this specific line of code

type TwFbObjKeys = keyof Twitter<string> | Facebook<string>
// is of type type TwFbObjKeys = "name" | "user" | "sex" | Facebook<string>

You may need to update it to

type TwFbObjKeys = keyof Twitter<string> | keyof Facebook<string>
// of type type TwFbObjKeys = "name" | "user" | "sex" | "appName" | "whatever" | "imjustanexapmle"

Answer №3

(Let's assume you're talking about

type TwFbObjKeys = keyof Twitter<string> | keyof Facebook<string>
)

In Typescript, an error is raised because paramId has a type that includes all the keys from both interfaces:

'name' | 'user'| 'sex' | 'appName' | 'whatever' | 'imjustanexapmle'

however, queryParameters can only belong to one of these types:

'name' | 'user'| 'sex'

or

'appName' | 'whatever' | 'imjustanexapmle'

To resolve this issue, it's necessary to separate these types into distinct interfaces:

interface Twitter<T> {
  name: T;
  user: T;
  sex: T extends string ? boolean : string;
}
interface Facebook<T> {
  appName: T;
  whatever: T;
  imjustanexapmle: T;
}

function isTwitter<T>(val: Twitter<T> | Facebook<T>): val is Twitter<T> {
  return (
    'name' in (val as Twitter<T>) &&
    'user' in (val as Twitter<T>) &&
    'sex' in (val as Twitter<T>) 
  )
}

function isFacebook<T>(val: Twitter<T> | Facebook<T>): val is Facebook<T> {
  return (
    'appName' in (val as Twitter<T>) &&
    'whatever' in (val as Twitter<T>) &&
    'imjustanexapmle' in (val as Twitter<T>) 
  )
}

function getValue<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

function getArrayOfObjects(
    queryParameters: Twitter<string> | Facebook<string>,
) {
  if (isTwitter(queryParameters)) { // if queryParameters is of type Twitter<string>
    const keys = Object.keys(queryParameters);
    return keys.map((paramId) => ({
      paramId,
      value: getValue(queryParameters, paramId as keyof Twitter<string>)
    }));
  } else if (isFacebook(queryParameters)) { // if queryParameters is of type Facebook<string>
    const keys = Object.keys(queryParameters);
    return keys.map((paramId) => ({ 
      paramId,
      value: getValue(queryParameters, paramId as keyof Facebook<string>)
    }));
  } else {  // if queryParameters is invalid
    throw new Error("invalid queryParameters!")
  }
}

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

Validating mixed types and generics within an array using Typescript's type checking

I am currently working with a setup that involves defining interfaces for animals and their noises, along with classes for specific animals like dogs and cats. I am also storing these animals in an array named pets. interface Animal<T> { name: stri ...

Looking for a regular expression to verify if the URL inputted is valid in TypeScript

After conducting thorough research, I discovered that none of the suggested URLs met my criteria, prompting me to raise a new query. Here are my specific requirements: * The URL may or may not include 'http' or 'https' * The URL can co ...

Using Cypress.Promise in a Cypress command causes type conflicts

When using Cypress 8.x.x, the following Cypress command works fine: declare global { namespace Cypress { interface Chainable<Subject> { login(): Chainable<Token>; } } } Cypress.Commands.add('login', () => { ret ...

For every piece of information in the reply

In the past, my method of working with JSON looked like this: this.http.get('http://localhost:3000/SumOfDipVolume').subscribe(res => { for (let dipVolume of res['result']) { // some code }); However, I have now realized that ...

Utilizing RxJs operators in Angular: utilizing async pipe and tap does not fill data

Here is a code snippet I've been working on: This snippet is written in Typescript: isDataSearch = false; getDatacollectionByID() { const params = { id: <some random ID>, }; this.metaData = this.dataService.getDatacollectionByID(par ...

Is it feasible to securely remove an item from an array within an object without the need for any assertions in a single function?

My interest in this matter stems from curiosity. The title may be a bit complex, so let's simplify it with an example: type ObjType = { items: Array<{ id: number }>; sth: number }; const obj: ObjType = { sth: 3, items: [{ id: 1 }, { id: 2 } ...

What is the process for accessing my PayPal Sandbox account?

I'm having trouble logging into my SandBox Account since they updated the menu. The old steps mentioned in this post Can't login to paypal sandbox no longer seem to work. Could someone please provide me with detailed, step-by-step instructions o ...

Problems with the duration of Shadcn Toasts (Inspired by the react-hot-toast library)

Within a 13.4 Nextjs project (app router), utilizing Typescript and TailwindCSS. I am currently exploring the usage of toasts provided by the remarkable shadcnUI Library, which draws inspiration from react-hot-toast while adding its own unique flair. Imp ...

Using a module without a declaration file: tips for troubleshooting

I am working on a Node.js project using Typescript and would like to incorporate the npm package country-code-lookup. However, this package does not have type declarations available. Despite the lack of typings, I still want to use this package in my proj ...

TypeScript enabled npm package

I am currently developing a npm module using TypeScript. Within my library, I have the following directory structure: . ├── README.md ├── dist │ ├── index.d.ts │ └── index.js ├── lib │ └── index.ts ├── ...

Retrieve only data that results in either a 1 or 0 when filtering

Currently, I am utilizing Angular2 with Typescript and making use of the filter method. The functionality of the filter() method involves creating a new array that contains elements which successfully pass the test defined by the given function. However, ...

Deleting specific URLs in Markdown using TypeScript

Currently, I am working with Typescript and Node v8, aiming to process markdown text and eliminate all image URLs that match specific criteria: URLs containing the term "forbidden" URLs with IP addresses I have been using the following regular expression ...

Troubleshooting: Angular 6 Renderer2 Issue with Generating Dynamic DOM Elements for SELECT-Option

Currently, I am attempting to dynamically create a select option using Renderer2. Unfortunately, I am facing difficulties in creating the <Select></Select> element, but I can confirm that the <options> are being successfully created. Due ...

Determine whether an interface includes a mandatory field

Can Typescript's Conditional Types be used to determine if an interface includes a required field? type AllRequired = { a: string; b: string } type PartiallyRequired = { a: string; b?: string } type Optional = { a?: string; b?: string } // Can we mo ...

Is it possible to pass a JSON key as a string in the @Input() decorator in Angular?

I am currently working on implementing angular material autocomplete in a custom component that can be easily used throughout my code. The challenge I am facing is setting up dynamic arrays with different keys for each array. Here is what I have attempted ...

The animation feature of the CountTo Module fails to function properly once the currency pipe is included

My end goal is to achieve a final view that looks like this: https://i.sstatic.net/VUGdb.png The values are supposed to have a counter animation from 0 to the specified final value. I have also implemented a currency pipeline in the value section for sep ...

Why is TypeScript unable to locate the identifier 'Express'?

Embarking on an express application using TypeScript. Displayed below is the code in app.ts: import express = require("express"); let app: Express = express(); I've successfully installed Express by using npm install --save express @types/expres ...

The task of mapping an array of objects with nested values using JavaScript is proving to

Attempting to convert an array of objects with nested values in child objects like: const objs = [{ "B": { "value": 1, }, "D": { "value": "45" }, "E": { "value": "234" }, ...

Exploring the functionalities of React Native with react-hook-form and implementing them with TypeScript

I've been working on creating a custom Input component in react native using typescript for the react-hook-form library. type CustomInputProps = { name: any, control: any } const CustomInput: FC<CustomInputProps> = ({name, control, ...p ...

How to retrieve the default type returned by a function using a custom TypeMap

I have a function that returns a promise with a type provided by generics. const api = <Model>(url: string) => Promise<Model> Currently, I always need to set the type of the returned data. const data = await api<{id: string, name: string ...