3

I am looking to get the "inverse" of a TypeScript mapped type (whose properties are strictly strings, so as to be "inversible"). To illustrate my desired result, I need a generic type

type Inverse<M> = ...

to be able to transform

type MappedType = {
  key1: 'value1'
  key2: 'value2'
};

into

/**
 * {
 *   value1: 'key1';
 *   value2: 'key2';
 * }
 */
type MappedTypeInverse = Inverse<MappedType>

I've tried a couple things already.. but to no avail:

type Inverse<M> = M extends Record<infer O, infer T> ? Record<T, O> : never;

/**
 * type MappedTypeInverse = {
 *   value1: 'key1' | 'key2'
 *   value2: 'key2' | 'key2'
 * }
 */
type MappedTypeInverse = Inverse<MappedType>
type InverseValue<M extends Record<any, any>, V extends M[keyof M]> = V extends M[infer K] ? K : never;

/**
 * type MappedTypeInverseValue = unknown // expecting 'key1'
 */
type MappedTypeInverseValue = InverseValue<MappedType, 'value1'>

Is this even possible? Any help would be appreciated!

feihcsim
  • 1,444
  • 2
  • 17
  • 33
  • How would you want to handle the inversion of `type MappedType = { key1: 'value1'; key2: 'value1'; };`? – Patrick Roberts Nov 26 '19 at 20:24
  • I am working on an application that not only receives queries but also sends queries. However, the format of the incoming queries differ from that of the outgoing ones because they work with different third-party services. I want to be able to handle this two-way conversion in a type-safe manner – feihcsim Nov 26 '19 at 20:33

3 Answers3

4

Here is a lean alternative (in addition to Patrick Robert's good solution):

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

// we assume the type to be an object literal with string values
// , should also work with number or symbol
type Inverse<M extends Record<string, string>> = {
  [K in M[keyof M]]: KeyFromVal<M, K>
};

type MappedType = {
  key1: 'value1'
  key2: 'value2'
};

type MappedTypeInverse = Inverse<MappedType> // { value1: "key1"; value2: "key2"; }
ford04
  • 66,267
  • 20
  • 199
  • 171
  • 2
    One thing to note is that this outputs a union of keys when the input mapped type has duplicate values, e.g. `Inverse<{ key1: 'value1'; key2: 'value1' | 'value2'; }> == { value1: 'key1' | 'key2'; value2: 'key2'; }` instead of `{ value1: never; value2: key2; }`. For some cases that behavior may even be preferable though, so good answer. – Patrick Roberts Nov 26 '19 at 22:30
  • 1
    Thank you and good hint. Yeah, I also would consider that behavior use-case dependent. If some strict check is need, we could borrow `IsUnion` from [here](https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union) and do something like this : `type InverseStrict> = { [K in M[keyof M]]: IsUnion> extends true ? never: KeyFromVal };`. That would emit a `never` type for the property value of the `Inverse`, if we get a union of keys (duplicate values in the source object). – ford04 Nov 26 '19 at 22:46
2

Here's how you can accomplish this. It borrows some "evil magic" from this answer to convert a union to an intersection at an intermediate step in the process:

type MappedType = {
    key1: 'value1';
    key2: 'value2';
};

type Intermediate<R extends Record<string, string>> =
    R extends Record<infer K, string>
    ? { [P in K]: { [Q in R[P]]: P; }; }
    : never;

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
    ? I
    : never;

type Inverse<R extends Record<string, string>> =
    Intermediate<R> extends Record<string, infer T>
    ? { [K in keyof UnionToIntersection<T>]: UnionToIntersection<T>[K]; }
    : never;

type InverseMappedType = Inverse<MappedType>;
// type InverseMappedType = {
//     value1: 'key1';
//     value2: 'key2';
// }

Another benefit of this approach is it outputs a mapped type with an appropriate property value never when the input record contains duplicate property values:

type MappedType = {
    key1: 'value1';
    key2: 'value1' | 'value2';
};

type InverseMappedType = Inverse<MappedType>;
// type InverseMappedType = {
//     value1: never;
//     value2: 'key2';
// }

Someone more well-versed than me in TypeScript may know a shorter method than this to inverse a mapped type, but this seems to get the job done at least.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
1

You can use mapped types with the as operator.

See playground example.

type Inverse<T> = {[K in keyof T as (T[K] & (string | number))]: K};
Meir
  • 14,081
  • 4
  • 39
  • 47
  • 1
    This worked well for me. I ended up defining it like this: `type Inverse> = {[K in keyof T as T[K]]: K};`. – JHH May 24 '23 at 08:42
  • @JHH this will not work for arrays, if you want an example that works for arrays as well, see this playground example - https://www.typescriptlang.org/play?#code/C4TwDgpgBAShBuEBOBnAhgIwDYQGpqwFdoBeKFYJASwDsBzKAHyhsIFsNkBuAKFElgJk6bBAAq4UoMSpMOfEQgBtALpNBAYwD2SACYAeCtXoAaacLl4CxAHy9+0ODJQQAYlQhYDYm1DJioCAAPYAgaXRRzWVEFYlUoAH4oMSVWDmQ1AC4okXlrCHtJKABJGmdxSX0A4NDwyKcLUQlIXzIAbyUAaShaKABrCBAtADNkqDRIlM61ADIctw8vKpsVbIBRII0iXQh9TpM0ziQbAF9ePiKxAEY-KDag7KursxBsgHJi8bYoEDezAE5AdkAGygs4XATXT5kUrlZq7a52HgAemRUHRAD0EjwIdAxAAmW5KN5BP5QN5oNBkgDs1LMAFpaWYSUFSSpCpD8dCSmVhBVIFV8UjUeioFieEA – Meir May 26 '23 at 06:29