Skip to content

Instantly share code, notes, and snippets.

@robkuz
Last active November 19, 2021 19:19
Show Gist options
  • Save robkuz/f52cbcd511593458e8470704534ba23e to your computer and use it in GitHub Desktop.
Save robkuz/f52cbcd511593458e8470704534ba23e to your computer and use it in GitHub Desktop.
Building a typesafe Lookup with Constant Objects
//I find typescripts enums a bit lacking.
//This is a way to havevery strongly typed bidirectional lookup table
//first we start with a helper type to reverse a record with unique literal value types as property types
type UniqueReverser<T extends Record<keyof T, PropertyKey>> = unknown extends {
[K in keyof T]-?: T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? unknown : never
}[keyof T] ? never : { [P in keyof T as T[P]]: P }
//this will allow to convert an object in this shape and type
const SOURCE = {
field1: "fieldA",
field2: "fieldB",
field3: "fieldC"
} as const
type SourceType = typeof SOURCE
/*
{
field1: "fieldA";
field2: "fieldB";
field3: "fieldC";
}
*/
//into its Reverse Type
type Reversed = UniqueReverser<typeof Source>
//will generate
/*
{
fieldA: "field1";
fieldB: "field2";
fieldC: "field3";
}
*/
//but only IF all property types are unique values.
//For example the following SOURCE will yield never
//as the type for property "field3" is the same type as for property "field1".
//each of them being fo type/literal value "fieldA"
const SOURCE = {
field1: "fieldA",
field2: "fieldB",
field3: "fieldA"
} as const
type Reversed = UniqueReverser<typeof Source>
//Reversed is never
//now we need to be able to actually reverse the source object into a target object
export function reverse<T extends Record<PropertyKey, PropertyKey>>(x: T): UniqueReverser<T> {
const keys: (keyof T)[] = Object.keys(x)
const result: any = {}
for (const key of keys) {
result[x[key]] = key
}
return (result as any as UniqueReverser<T>)
}
//so that the following code
const SOURCE = {
field1: "fieldA",
field2: "fieldB",
field3: "fieldC"
} as const
const TARGET: UniqueReverser<typeof SOURCE> = reverse(SOURCE)
TARGET.fieldC //yields the cvalue = "field3"
//now we combine all of that in small data structure and have a factory for it
function buildLookup<T extends Record<PropertyKey, PropertyKey>>(x: T) {
const target = reverse(x)
type Lookup = {
readonly source: T;
readonly target: UniqueReverser<T>
}
const result: Lookup = {
source: x,
target
}
return result
}
//and finally we have our bidrectional strongly typed lookup table
const lookup = buildLookup(SOURCE)
lookup.source.field1 // will yield the value "fieldA"
lookup.target.fieldA // will yield the value "field1"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment