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

Tips on using services for storing and fetching list data in Angular

I currently have two components, the "add-expense" component and the "view-list" component. The "add-expense" component collects expense details from a form and stores them as an object. My goal is to add this object to an empty list within the "expense-li ...

What is the equivalent of getElementById in .ts when working with tags in .js?

Looking to incorporate Electron, Preload, Renderer with ReactJS and TypeScript into my project. <index.html> <body> <div id="root" /> <script src='./renderer.js'/> </body> <index.ts> const root = Re ...

Integrate Angular 2 components into WebStorm

I am currently working on a project using Angular 2 (rc5) and TypeScript (1.8.10). Angular 2 is built with TypeScript, but in the node_modules directory, I notice that there are JavaScript files (*.js) along with declaration files (*.d.ts). It makes it di ...

How can I enable editing for specific cells in Angular ag-grid?

How can I make certain cells in a column editable in angular ag-grid? I have a grid with a column named "status" which is a dropdown field and should only be editable for specific initial values. The dropdown options for the Status column are A, B, C. When ...

Adding a constant to a Vue component

Currently working on developing an application using Vue and TypeScript. I'm focused on refactoring some aspects, particularly moving hard-coded strings from a template to a separate constant. What has been implemented is as follows: export const va ...

The tRPC setData hook is limited in its ability to access all data necessary for optimistic UI updates

As I integrate mutations using tRPC and React Query to optimistically update my UI upon adding a new item, I've encountered an issue. The problem lies in the query I'm updating, which requires specific properties like auto-generated IDs or datab ...

What are the steps to implement the `serialport` library in `deno`?

After tinkering with Deno to extract readings from an Arduino, I encountered a roadblock when it came to using the serialport library correctly. Here is what I attempted: According to a post, packages from pika.dev should work. However, when trying to use ...

Ways to resolve the issue: ""@angular/fire"' does not contain the exported member 'AngularFireModule'.ts(2305) in an ionic, firebase, and

I am facing an issue while attempting to establish a connection between my app and a firebase database. The problem arises as I receive 4 error messages in the app.module.ts file: '"@angular/fire"' has no exported member 'AngularFi ...

When a function is transferred from a parent component to a child component's Input() property, losing context is a common issue

I have encountered an issue while passing a function from the parent component to the child component's Input() property. The problem arises when the parent's function is called within the child component, causing the this keyword to refer to th ...

"Extra loader required to manage output from these loaders." error encountered in React and Typescript

After successfully writing package 1 in Typescript and running mocha tests, I confidently pushed the code to a git provider. I then proceeded to pull the code via npm into package 2. However, when attempting to run React with Typescript on package 2, I enc ...

Tips for managing variables to display or hide in various components using Angular

In this example, there are 3 main components: The first component is A.component.ts: This is the parent component where an HTTP call is made to retrieve a response. const res = this.http.post("https://api.com/abcde", { test: true, }); res.subscribe((r ...

Angular/TypeScript restricts object literals to declaring properties that are known and defined

I received an error message: Type '{ quantity: number; }' is not assignable to type 'Partial<EditOrderConfirmModalComponent>'. Object literal may only specify known properties, and 'quantity' does not exist in type &ap ...

Enhancing the appearance of the Mui v5 AppBar with personalized styles

I am encountering an issue when trying to apply custom styles to the Mui v5 AppBar component in Typescript. import { alpha } from '@mui/material/styles'; export function bgBlur(props: { color: any; blur?: any; opacity?: any; imgUrl?: any; }) { ...

Error when casting Typescript await toPromise

I encountered the following issue: export class FloorManagerComponent implements OnInit { public meta = { list: [], building: Building, loading: true, }; constructor( private router: Router, private ac ...

What are the recommended guidelines for using TypeScript effectively?

When facing difficulties, I have an array with functions, such as: this._array = [handler, func, type] How should I declare this private property? 1. Array<any> 2. any[] 3. T[] 4. Array<T> What is the difference in these declarations? ...

Using JSON data in an ArrayBuffer with TypeScript

I am currently struggling with converting the received ArrayBuffer data from a server via Websocket into another format. Below is the WebSocket code snippet: let ws = new WebSocket('wss://api.example.com/websocket'); ws.binaryType = 'arrayb ...

Using *ngFor to populate an array in an ion-list within Ionic 2

Hi there, I'm currently learning Ionic 2 and I recently created an array that I want to loop through in an ion-list. This is my produk.ts import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angul ...

Exploring generic types using recursive inference

The scenario: export type SchemaOne<T> = | Entity<T> | SchemaObjectOne<T>; export interface SchemaObjectOne<T> { [key: string]: SchemaOne<T>; } export type SchemaOf<T> = T extends SchemaOne<infer R> ? R : nev ...

What is the correct way to format React's dispatch function in order to utilize a "then" method similar to a Promise?

I'm working on a simple app that dispatches an action upon first load to populate the store. However, I'm facing an issue with trying to run a then method on dispatch, as typescript is throwing errors. (As per redux's documentation, the ret ...

Encountering issues while attempting to run an npm install command on the package.json file

Having trouble running npm install to set up my Angular project on a Mac. It seems like the issues are due to working with an older project. npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve npm ERR! npm ERR! While resolving: @angular-devkit/< ...