0

I have a function which takes a map and generates the bijection of the map:

 export function keyTextBijection(map) {
  const bijection = {};
  Object.keys(map).forEach(key => {
    bijection[key] = map[key];
    bijection[map[key]] = key;
  });
  return bijection;
}

How to signature the function elegantly in TypeScript?

SWAN ZHANG
  • 31
  • 1

1 Answers1

1

It helps to write a type function to turns a type like {a: "A", b: "B"} to its inverse, {A: "a", B: "b"}. TypeScript 4.1 will make this easy with mapped type as clauses as implemented by microsoft/TypeScript#40336:

// TS4.1+
type Invert<T extends Record<keyof T, PropertyKey>> = { [K in keyof T as T[K]]: K };

But for TypeScript 4.0 and below you can get something reasonably equivalent in most cases by using mapped conditional types:

// TS4.0-
type Invert<T extends Record<keyof T, PropertyKey>> =
    { [K in T[keyof T]]: { [P in keyof T]: K extends T[P] ? P : never }[keyof T] }

In either case, you want keyTextBijection to take an object of type T and return an object of type T & Invert<T>. Since such an intersection looks ugly, I usually define a no-op mapped type called Id which has the effect of merging the intersection into a single type object. (This Id is also called Expand in this question):

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : any;

So now we can give a call signature to keyTextBijection:

function keyTextBijection<T extends Record<keyof T, S>, S extends PropertyKey>(
  map: T
): Id<T & Invert<T>>;
function keyTextBijection(map: any) {
    const bijection: any = {};
    Object.keys(map).forEach(key => {
        bijection[key] = map[key];
        bijection[map[key]] = key;
    });
    return bijection;
}

In the above, S doesn't really do anything for inference purposes; it's mostly just a way to give the compiler a hint that it should try to infer the T type as a mapping from literals to literals. When you enter {a: "A"} as the map argument, you want the compiler to see the type as {a: "A"} and not as {a: string}, since the latter will not invert properly.

And the function uses a single overload call signature to allow the implementation to be lax with type safety and use any. (If you don't do this, you'll find yourself having to use a bunch of type assertions because the compiler won't be able to verify the type safety of the assignment statements).


Let's test it:

const bij = keyTextBijection({ a: "A", b: "B", c: "C" });
/* const bij: {
    a: "A";
    b: "B";
    c: "C";
    A: "a";
    B: "b";
    C: "c";
} */
console.log(bij);
/* {
  "a": "A",
  "A": "a",
  "b": "B",
  "B": "b",
  "c": "C",
  "C": "c"
}  */

The type and the value of bij look correct to me.


Playground link to code

jcalz
  • 125,133
  • 11
  • 145
  • 170