Can TypeScript accurately perform the _.invert function?

When using lodash, you can utilize the _.invert function to switch an object's keys and values:

var object = { 'a': 'x', 'b': 'y', 'c': 'z' };

_.invert(object);
// => { 'x': 'a', 'y': 'b', 'z': 'c' }

The current definitions in the lodash typings always specify a stringstring mapping for this function:

_.invert(object);  // type is _.Dictionary<string>

However, in certain cases, particularly when utilizing a const assertion, a more accurate type might be necessary:

const o = {
  a: 'x',
  b: 'y',
} as const;  // type is { readonly a: "x"; readonly b: "y"; }
_.invert(o);  // type is _.Dictionary<string>
              // but it would ideally be { readonly x: "a", readonly y: "b" }

Is there a way to achieve typings that precise? The following declaration comes close:

declare function invert<
  K extends string | number | symbol,
  V extends string | number | symbol,
>(obj: Record<K, V>): {[k in V]: K};

invert(o);  // type is { x: "a" | "b"; y: "a" | "b"; }

While the keys are correct, the values result in a union of input keys, meaning the specificity of the mapping is lost. Is it possible to achieve perfect precision in this scenario?

Answer №1

Revise

With the implementation of the as clause in mapped types, you have the ability to define this type as follows:

You can utilize a mapped type with an as clause:



type InvertResult<T extends Record<PropertyKey, PropertyKey>> = {
  [P in keyof T as T[P]]: P
}

Playground Link

Initial response

You can achieve this using a more complex mapped type that maintains the accurate value:

const o = {
    a: 'x',
    b: 'y',
} as const;

type AllValues<T extends Record<PropertyKey, PropertyKey>> = {
    [P in keyof T]: { key: P, value: T[P] }
}[keyof T]
type InvertResult<T extends Record<PropertyKey, PropertyKey>> = {
    [P in AllValues<T>['value']]: Extract<AllValues<T>, { value: P }>['key']
}
declare function invert<
    T extends Record<PropertyKey, PropertyKey>
>(obj: T): InvertResult<T>;

let s = invert(o);  // type is { x: "a"; y: "b"; }

Playground Link

AllValues initially creates a union containing all key, value pairs (for example:

{ key: "a"; value: "x"; } | { key: "b"; value: "y"; }
). In the mapped type, we iterate over all value types in the union and extract the original key for each value using Extract. This method works effectively unless there are duplicate values, in which case it results in a union of keys where the value appears.

Answer №2

Titian Cernicova-Dragomir's approach is quite impressive. I recently discovered another method to interchange object keys and values using Conditional Types:

type KeyFromValue<V, T extends Record<PropertyKey, PropertyKey>> = {
  [K in keyof T]: V extends T[K] ? K : never
}[keyof T];

type Invert<T extends Record<PropertyKey, PropertyKey>> = {
  [V in T[keyof T]]: KeyFromValue<V, T>
};

Try it out with the object const o:

const o = {
  a: "x",
  b: "y"
} as const;

// type Invert_o = {x: "a"; y: "b";}
type Invert_o = Invert<typeof o>;

// works
const t: Invert<typeof o> = { x: "a", y: "b" };
// Error: Type '"a1"' is not assignable to type '"a"'.
const t1: Invert<typeof o> = { x: "a1", y: "b" };

Define the function invert following the same pattern as the above solution with a Return type of Invert<T>.

Playground

Answer №3

Implementing this feature is made much easier thanks to the introduction of Key Remapping in Mapped Types in TypeScript 4.1:

const data = {
    name: 'John',
    age: 30
} as const;

declare function reverse<
    T extends Record<PropertyKey, PropertyKey>
>(input: T): {
    [K in keyof T as T[K]]: K
};

let output = reverse(data);  // resulting type will be { readonly John: "name"; readonly 30: "age"; }

playground

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

Errors can occur when using TypeScript recursive types

Below is a simplified version of the code causing the problem: type Head<T> = T extends [infer U,...unknown[]] ? U : never; type Tail<T> = T extends [unknown,...infer U] ? U : []; type Converter = null; type Convert<T, U extends Converter& ...

After verifying the variable is an Array type, it is ideal to utilize the .forEach()

Within my generic functional component, I have the following code snippet: if(Array.isArray(entry[key as keyof T]) { entry[key as keyof T].forEach((item: T) => { ... }); } The variable key is a string that dynamically changes. However, when attempt ...

Troubleshooting Axios errors when using createAsyncThunk function

Can someone help me with handling errors in createAsyncThunk using TypeScript? I attempted to declare the returned type and params type with generics, but when it came to error handling typing, I found myself resorting to just using 'any'. Let& ...

Having trouble getting Lodash find to work properly with TypeScript overload signatures

I have been attempting to utilize _.find in TypeScript on an Object in order to retrieve a value from this object. The original code snippet looked like this: const iconDict = { dashboard: <DataVisualizer />, settings: <SettingsApp /> ...

Unreliable TypeScript errors when using spread arguments

Consider this function: function foo(a: number, b: number) {/* ... */} Error is triggered by Snippet 1: foo(1, ...[]); Expected 2 arguments, but received only 1. Error is triggered by Snippet 2: foo(1, 2, ...[]); Expected 2 arguments, but rece ...

Tips for integrating Typescript Definition files with Visual Studio 2017

I have a challenge with my ASP.NET Core 2.0 application where I am attempting to incorporate TypeScript and jQuery. While TypeScript integration has been successful, I am facing issues with jQuery as it does not provide me with intellisense. Despite trying ...

Encountering a navCtrl problem in Ionic 3 while attempting to utilize it within a service

I am currently working on a feature to automatically route users to the Login Page when their token expires. However, I am encountering an issue with red lines appearing under certain parts of my code. return next.handle(_req).do((event: HttpEvent< ...

Cannot execute loop

I'm currently working on creating a loop within my component that involves making server calls: getBeds() { this.patientService.getBeds(this.selectedWard).subscribe( result => { console.log(result); this.beds = result; this.getBedDet ...

What is the process for setting the values of an object within a constructor to all class properties?

I am attempting to easily transfer all the properties from an object in a constructor to a class's properties type tCustomUpload = { name : string, relationship : string, priority : number, id : number } class CustomUpload { name : ...

When utilizing DomSanitizer, Angular2 suddenly ceases to function properly

I've been working with Angular 2 and TypeScript. Everything was going well until I encountered an issue with my pipe, which is causing the DomSanitizer to interfere with the (click) event functionality. Even though the (click) code appears intact in ...

Display captions on react-player videos using an .srt file

Currently, I am working on a React/Typescript project with Next.js. A key feature of this project is a modal that utilizes 'react-player' to display videos. While the video and modal are functioning as intended, I am looking to incorporate capti ...

Triggering actionsheet button clicks in Ionic 3

I need assistance with adding buttonClick functionality to actionSheet buttons in ionic 3. When a button is clicked, I want to open a modal (custom alert). Below is the code snippet: openActionSheet() { console.log('opening'); le ...

Display <video> component using Angular 2

When attempting to display videos sourced from an API link, I encountered a problem where only the player was visible without the actual video content. The navigation controls of the player were also unresponsive. Interestingly, when manually inputting the ...

Electron does not have the capability to utilize Google's Speech to Text engine

I am trying to connect my microphone with the Google Speech to Text engine. I came across this page and copied the code into my renderer.ts file, uncommented the lines with const, but when I run it, I encounter an error at line 7 (const client = new speech ...

Guide on accessing a modal component in Angular?

I have an Edit Button on my component called SearchComponent. When the user clicks this button, it currently redirects them to another component named EditFormComponent using navigateByUrl('url-link'). However, I would like to enhance the user ex ...

The toISOString() method is deducting a day from the specified value

One date format in question is as follows: Tue Oct 20 2020 00:00:00 GMT+0100 (Central European Standard Time) After using the method myValue.toISOString();, the resulting date is: 2020-10-19T23:00:00.000Z This output shows a subtraction of one day from ...

Develop a custom function in Typescript that resolves and returns the values from multiple other functions

Is there a simple solution to my dilemma? I'm attempting to develop a function that gathers the outcomes of multiple functions into an array. TypeScript seems to be raising objections. How can I correctly modify this function? const func = (x:number, ...

How come the value passed to the component props by getServerSideProps is incorrect?

I have been facing an issue while trying to retrieve data from 4 different endpoints and then passing them as props using getServerSideProps in Next.js. Even though the "courses" variable returned from getServerSideProps does contain the necessary course ...

Does the class effectively implement the interface even if the method of a member variable has undefined arguments?

Let's take a closer look at my code, which lacks proper descriptions. Here is the interface: interface IModel<T = any> { effects: { [key: string]: (getState: () => T) => void; }; } interface IState { name: string; age: numbe ...

Create an abstract method that will return the properties of the constructor

To establish an abstract class in typescript, we can name it Entity, which contains an abstract method called constructorProps() that returns the array of properties required to build the derived class. For instance, if Foo extends Entity and does not hav ...