export const none = Symbol(); export type None = typeof none; const MEMBER_TYPE = Symbol(); export type MemberType< TaggedUnionT extends { [MEMBER_TYPE]: any; } > = TaggedUnionT[typeof MEMBER_TYPE]; type DefObjSuper = { [key: string]: None | ((...args: any[]) => any) }; type DataMap = { [Property in keyof DefObj]: DefObj[Property] extends (...args: any) => any ? ReturnType : DefObj[Property] extends None ? undefined : DefObj[Property]; }; type CasesObjFull = { [Property in keyof DataMap]: DataMap[Property] extends None ? () => any : (data: DataMap[Property]) => any; }; type if_you_are_seeing_this_then_your_match_didnt_either_handle_all_cases_or_provide_a_default_handler_using_underscore< DefObj extends DefObjSuper > = Partial> & { _: >( data: DataMap[Property] ) => any; }; type MatchConfiguration = | CasesObjFull | if_you_are_seeing_this_then_your_match_didnt_either_handle_all_cases_or_provide_a_default_handler_using_underscore< DefObj >; class MemberObjectImpl { variant: any; data: any; constructor(variant: any, data: any) { this.variant = variant; this.data = data; } match(casesObj: any): any { const data = this.data; const matchingHandler = casesObj[this.variant]; if (matchingHandler) { return matchingHandler(data); } else if (casesObj._) { return casesObj._(data); } else { throw new Error(`Match did not handle variant: '${this.variant}'`); } } } Object.defineProperty(MemberObjectImpl, "name", { value: "MemberObject" }); interface MemberObject { match>( casesObj: C ): ReturnType>; variant: keyof DefObj; data: DataMap[keyof DefObj]; } export type TaggedUnion = { [Property in keyof DefObj]: DefObj[Property] extends (...args: any) => any ? (...args: Parameters) => MemberObject : MemberObject; } & { [MEMBER_TYPE]: MemberObject; }; export function makeTaggedUnion( defObj: DefObj ): TaggedUnion { const matchObj: any = {}; Object.keys(defObj).forEach((matchType) => { const value = defObj[matchType]; if (typeof value === "function") { matchObj[matchType] = (...args: any) => { const data = value(...args); return new MemberObjectImpl(matchType, data); }; } else { matchObj[matchType] = new MemberObjectImpl(matchType, undefined); } }); return matchObj; }