Created
June 17, 2022 14:12
-
-
Save steinerkelvin/67e05fe1a470420696a6e73e83801d8f to your computer and use it in GitHub Desktop.
Typescript typings and utility functions for Rust's serde / serde_json serialized enums (tagged unions)
This file contains hidden or 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
// Some magic from | |
// https://stackoverflow.com/questions/65750673/collapsing-a-discriminated-union-derive-an-umbrella-type-with-all-possible-key | |
// I replaced `undefined` usages with the placeholder below so we can exclude | |
// it properly on the last definition. | |
declare const PLACEHOLDER: unique symbol | |
type Placeholder = typeof PLACEHOLDER | |
// Magic as far as I'm concerned. | |
// Taken from https://stackoverflow.com/a/50375286/3229534 | |
type UnionToIntersection<U> = | |
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never | |
// This utility lets T be indexed by any key | |
type Indexify<T> = T & { [str: string]: Placeholder; } | |
// To make a type where all values are undefined, so that in AllUnionKeys<T> | |
// TS doesn't remove the keys whose values are incompatible, e.g. string & number | |
type UndefinedVals<T> = { [K in keyof T]: undefined } | |
// This returns a union of all keys present across all members of the union T | |
type AllUnionKeys<T> = keyof UnionToIntersection<UndefinedVals<T>> | |
// Where the (rest of the) magic happens ✨ | |
type AllFields<T> = { [K in AllUnionKeys<T> & string]: Exclude<Indexify<T>[K], Placeholder> } | |
// End of Magic | |
// Enum TS library | |
// =============== | |
type Key = string | number | symbol | |
type Variant<Tag extends Key, T> = { [x in Tag]: T } | |
type TagVariants<T> = { [K in keyof T]: Variant<K, T[K]> } | |
type Enum<T> = TagVariants<T>[keyof T] | |
type FlatVariant<Tag extends Key, T> = { $: Tag, val: T } | |
type FlatEnum<T> = { [K in keyof T]: FlatVariant<K, T[K]> }[keyof T] | |
type VariantsFrom<T> = AllFields<T> | |
type FlatEnumFrom<T> = FlatEnum<VariantsFrom<T>> | |
function flatten_enum<T>(value: T): FlatEnumFrom<T> { | |
for (let key in value) { | |
if (key !== '$') { | |
let _result = value[key] | |
let result = { $: key, val: _result } | |
return result as unknown as FlatEnumFrom<T>; | |
} | |
} | |
throw new Error(`Variant is empty: '${value}'.`) | |
} | |
const flatten_enum_f = | |
<V,>(value: V) => | |
<R,>(f: ((v: FlatEnumFrom<V>) => R)) => f(flatten_enum(value)) | |
type MatchDict<V, R = void> = { [tag in keyof V]: (v: V[tag]) => R } | |
const match = | |
<E,>(value: E) => | |
<R,>(matcher: MatchDict<VariantsFrom<E>, R>): R => | |
flatten_enum_f(value)(({ $, val }) => { | |
let arm = matcher[$] | |
return arm(val) | |
}) | |
const map_enum = <E,>(value: E) => <R,>(matcher: MatchDict<VariantsFrom<E>, R>): R => | |
flatten_enum_f(value)(({$, val}) => { | |
let arm = matcher[$] | |
return arm(val) | |
}) | |
const if_let = | |
<E,>(value: E) => | |
<K extends keyof VariantsFrom<E>>(tag: K) => | |
<R,>(th: (v: VariantsFrom<E>[K]) => R) => | |
(el: () => R): R => | |
flatten_enum_f(value)(({ $, val }) => ($ === tag ? th(val) : el())) | |
// Example Enum | |
// ============ | |
type Stuff = Enum<Stuff_Variants> | |
interface Stuff_Variants { | |
Color: Color | |
BW: BW | |
} | |
interface Color { | |
r: number | |
g: number | |
b: number | |
} | |
interface BW { | |
val: boolean | |
} | |
function test() { | |
let a: Stuff = { Color: { r: 10, g: 20, b: 35 } } | |
let b: Stuff = { BW: { val: true } } | |
let all: Stuff[] = [a, b] | |
for (let _elem of all) { | |
console.log(`Matching with flatten_enum...`) | |
let variant = flatten_enum(_elem) | |
switch (variant.$) { | |
case "Color": | |
let color = variant.val | |
console.log(color.r + color.g + color.b) | |
break; | |
case "BW": | |
let bw = variant.val | |
console.log(bw.val) | |
} | |
} | |
for (let elem of all) { | |
console.log(`Matching with flatten_enum_f...`) | |
flatten_enum_f(elem)(({$, val}) => { | |
switch ($) { | |
case "Color": | |
console.log(val.r + val.g + val.b) | |
break | |
case "BW": | |
console.log(val.val) | |
break | |
} | |
} | |
) | |
} | |
for (let elem of all) { | |
console.log(`Matching with map_enum...`) | |
match(elem)({ | |
Color: (color) => console.log(color.r + color.g + color.b), | |
BW: (bw) => console.log(bw.val), | |
}) | |
} | |
for (let elem of all) { | |
console.log(`Matching with if_let...`) | |
if_let(elem)("Color") | |
((color) => console.log(`This is color: R:${color.r} G:${color.g} B:${color.b}`)) | |
(() => console.log(`This is not color: ${elem}`)) | |
} | |
} | |
test() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TS playground