Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to precisely type _.invert in TypeScript?

In lodash, the _.invert function inverts an object's keys and values:

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

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

The lodash typings currently declare this to always return a stringstring mapping:

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

But sometimes, especially if you're using a const assertion, a more precise type would be appropriate:

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

Is it possible to get the typings this precise? This declaration gets 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"; }

The keys are right, but the values are the union of the input keys, i.e. you lose the specificity of the mapping. Is it possible to get this perfect?

like image 902
danvk Avatar asked Jun 02 '19 14:06

danvk


1 Answers

Edit

Since the introduction of the as clause in mapped types you can write this type as:


You can use a mapped type with an as clause:

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

Playground Link

Original answer

You can do it using a more complicated mapped type that preserves the correct 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 first creates a union that contains all key, value pairs (so for your example this will be { key: "a"; value: "x"; } | { key: "b"; value: "y"; }). In the mapped type we then map over all value types in the union and for each value we extract the original key using Extract. This will work well as long as there are no duplicate values (if there are duplicate values we will get a union of the keys wehere the value appears)

like image 129
Titian Cernicova-Dragomir Avatar answered Oct 22 '22 23:10

Titian Cernicova-Dragomir