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.