Trying to determine the specific key of an object based on its value in TypeScript?

Looking to create a function that can retrieve the key for a given value.

type Items<T> = Exclude<{
  [P in keyof T]: [P, T[P]];
}[keyof T], undefined>[];

export const getKeyName = <T extends Record<PropertyKey, unknown>, V>(
  collection: T,
  targetValue: V,
): V extends T[infer K extends keyof T] ? K : undefined => {
  const entry = (Object.entries(collection) as Items<typeof collection>).find(([_, v]) => v === targetValue);
  const keyName = entry?.[0];
  // @ts-expect-error bypassing current type inference limitation
  return keyName;
};

const list = {
  apple: 1,
  banana: 2,
} as const;

// Need the data type of `selectedFruit` to be "apple", not "apple" | "banana"
const selectedFruit = getKeyName(list, 1 as const);

In the final line, aiming for the data type of keyName to be "expectFruit", rather than "apple" | "banana".

Any suggestions on how to achieve this?

Answer №1

For the map object, only primitive values are allowed since === is used for comparison. This necessitates the use of the Primitives type:


type Primitives =
  | null
  | string
  | number
  | boolean
  | undefined
  | symbol
  | bigint

The Entries<T> type is also required when using entries:


// Link to Stack Overflow covers question on maintaining key-value relationship in object entries
type Entries<T> = {
    [K in keyof T]: [K, T[K]];
}[keyof T][];

const entries = <
  Obj extends Record<PropertyKey, unknown>
>(obj: Obj) =>
  Object.entries(obj) as Entries<Obj>

We can now proceed with implementing our function. It's important to note that to infer the exact return type, similar logic needs to be implemented at the type level as well. In other words, the same operations need to be done in the type scope.

Here are some type utilities that mimic the runtime function:

// Issue comment from Microsoft regarding checking if a type is never
type IsNever<T> = [T] extends [never] ? true : false

// Replace never with undefined if value is not found
type UseUndefined<T> = IsNever<T> extends true ? undefined : T

// Search for key by value within entries
type GetKeyByValue<
  Obj extends Record<PropertyKey, Primitives>,
  Value extends Primitives
> =
  UseUndefined<Extract<Entries<Obj>[number], [PropertyKey, Value]>[0]>

IsNever checks whether a type is "never". In this case, "never" indicates that the value has not been found.

UseUndefined replaces "never" with "undefined" if the value is not found.

GetKeyByValue searches for the appropriate tuple in entries with the expected value.

Let's see how it operates:


// Repeat definition of Primitives and Entries types for reference 
type Primitives =
  | null
  | string
  | number
  | boolean
  | undefined
  | symbol
  | bigint

// Link to Stack Overflow covers question on maintaining key-value relationship in object entries
type Entries<T> = {
  [K in keyof T]: [K, T[K]];
}[keyof T][];

const entries = <
  Obj extends Record<PropertyKey, unknown>
>(obj: Obj) =>
  Object.entries(obj) as Entries<Obj>

// Issue comment from Microsoft regarding checking if a type is never
type IsNever<T> = [T] extends [never] ? true : false

// Replace never with undefined if value is not found
type UseUndefined<T> = IsNever<T> extends true ? undefined : T

// Search for key by value within entries
type GetKeyByValue<
  Obj extends Record<PropertyKey, Primitives>,
  Value extends Primitives
> =
  Readonly<Obj> extends Obj ?
  UseUndefined<Extract<Entries<Obj>[number], [PropertyKey, Value]>[0]>
  : [123]

function getKeyByValue<
  Values extends Primitives,
  Obj extends Record<PropertyKey, Values>,
  V extends Primitives
>(
  map: Obj,
  value: V,
): GetKeyByValue<Obj, V> & Primitives
function getKeyByValue(
  map: Record<PropertyKey, Primitives>,
  value: Primitives,
): Primitives {
  const entry = entries(map).find(([_, v]) => v === value);
  const key = entry?.[0];
  return key;
};

// Example usage scenarios
const _ = getKeyByValue({
  foo: 1,
  boo: 2,
}, 1);

const __ = getKeyByValue({
  foo: 1,
  boo: 2,
}, 2);

const ___ = getKeyByValue({
  foo: 1,
  boo: 2,
}, 3)

Playground

If you're interested in inferring function arguments, my article on Type Inference on function arguments may be informative.

P.S. Function overloading was used due to conditional types not being supported within return types.


Could you explain why the generic parameter Values ...

This demonstrates how inference works on function arguments. When using a single generic, literal primitive types can be inferred:

const foo = <T,>(a: T) => a

// Literal type inference example
foo(42)

However, if an object is passed instead of a literal value like 42, and the generic is used without appropriate constraint, only the shape of the object can be inferred, not its specific values.

const foo = <T,>(a: T) => a

// Only infers object shape { num: number }
foo({ num: 42 })

To infer the literal type of an object's value, an additional generic is needed for that purpose:

const foo = <
  Val extends number,
  Obj extends { num: Val }
>(a: Obj) => a

// Infers specific object value { num: 42 }
foo({ num: 42 })

I have elaborated on this behavior in my article.

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

Select one of 2 parameters and begin typing

I recently encountered a situation where I needed to define a type with an id field (string) and an oldId field (number), but I wanted these fields to be exclusive. For example: { id: "1234", name: "foo" } { oldId: 1234, name: "b ...

Dispersed data points within ng2-charts

After reviewing the ng2-charts documentation at , unfortunately, I did not come across any information regarding a Scattered Plot. Are there alternative methods to create a Scattered plot chart in ng2-charts? Any helpful tricks or customization options a ...

The TypeScript error arises when an element implicitly contains an 'any' type due to the inability to use an expression of type 'any' to index a specific type

Encountering an Issue: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{ foo: string; bar: string; }'.ts(7053) Within the following code snippet: const CATEGORY_COLORS ...

Setting up a Node.js project in your local environment and making it

I need help installing my custom project globally so I can access it from anywhere in my computer using the command line. However, I've been struggling to make it work. I attempted the following command: npm install -g . and some others that I can&ap ...

Customizing number input types in Angular 2 - the perfect solution

Attempting to incorporate a time picker using HTML5 input type number in Angular2. The code snippet below illustrates the setup: <input type="number" [(ngModel)]="hour" (change)="checkHours();" name="one" min="1" max="12"> <input type="number" ...

Managing component composition in React/TypeScript: What's the best way to approach it?

I am brand new to the world of typescript, so please be patient with me. My objective is to transform this react component: interface ButtonProps {...} const Button: React.FC<ButtonProps> = ({ children, href, value as = 'button', ...

What is preventing type-graphql from automatically determining the string type of a class property?

I have a custom class named Foo with a property called bar that is of type string. class Foo { bar: string } When I use an Arg (from the library type-graphql) without explicitly specifying the type and set the argument type to string, everything works ...

Facing issues with integrating Mixpanel with NestJS as the tracking function cannot be located

While utilizing mixpanel node (yarn add mixpanel) in conjunction with NestJS, I have encountered an issue where only the init function is recognized. Despite calling this function, I am unable to invoke the track function and receive the error message: Ty ...

Tips for making a property non-nullable in Typescript

The Node built-in IncomingMessage type from DefinitelyTyped's definition (used as req in the (req, res, next) arguments) has specified that url can be nullable. This excerpt shows part of the definition: // @types/node/index.d.ts declare module "http ...

Detecting changes in input text with Angular and Typescript

Searching for a straightforward and practical method to identify changes in my textfield has been challenging. Avoiding the use of (keypress) is necessary, as users may occasionally paste values into the field. The (onchange) event only triggers when the u ...

tslint issues detected within a line of code in a function

I am a novice when it comes to tslint and typescript. Attempting to resolve the error: Unnecessary local variable - stackThird. Can someone guide me on how to rectify this issue? Despite research, I have not been successful in finding a solution. The err ...

What is the best way to determine the highest value?

How can I ensure that the data is displayed based on the condition c.date <= this.selectedReport.report_date? The current code snippet if (Math.max(...this.costs.map(c => c.date))){} seems to be causing an issue where no data is being displayed. What ...

flushMicrotasks does not function properly in conjunction with the image.onload event

Working on an Angular project, I'm currently developing an object with an image field. The method responsible for loading the image returns a promise that resolves in the onload function of the image. When trying to test this method using the flushMi ...

Eliminate the chosen and marked items from a list - Angular 2+/ Ionic 2

Currently, I have a checkbox list on my page. Whenever a user selects the "Save" button, the checked items should be removed from the list and moved to the saved tab that is also displayed. While I have successfully implemented the functionality for removi ...

Retrieve the text content from the HTML document

I'm facing a beginner's challenge. I have a div element and I want to extract the URL from the data-element attribute into a .json file Is there a way to do this? <div content="" id="preview" data-element="http://thereislink" class="sample ...

The constant issue persists as the test continues to fail despite the component being unmounted from the

import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { act } from 'react' import Notifications, { defaultNotificationTime, defaultOpacity, queuedNotificationTime, fa ...

Verifying data types in TypeScript

When working with TypeScript in the browser, I often find myself writing code like this: const button = document.getElementById(id); if (!(button instanceof HTMLButtonElement)) { throw new Error("TODO -- insert better error message here"); } bu ...

Implementing a feature in ReactJS that allows users to upload multiple images in base64 format

I'm trying to develop an image uploader using base64 and I want the output as an array. However, I am encountering an issue where the array is coming out empty!. I suspect it might be due to an asynchronous problem. Any tips on how to incorporate asyn ...

Verify whether a component is a React.ReactElement<any> instance within a child mapping operation

I am facing a challenge with a component that loops through children and wraps them in a div. I want to exclude certain elements from this process, but I am struggling to determine if the child is a ReactElement or not (React.ReactChild can be a string or ...

Utilize a generic data type for a property that can accept values of type 'number', 'string', or 'undefined'

This query involves React code but pertains to typescript rather than react. To simplify, I have a component called MyList which accepts a single generic type argument passed to the props type. The generic type represents an object that will be used to c ...