Last active
November 19, 2021 19:19
-
-
Save robkuz/f52cbcd511593458e8470704534ba23e to your computer and use it in GitHub Desktop.
Building a typesafe Lookup with Constant Objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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