From ec65921391bd718b9ba3dc6ac9d2706dd737f2ef Mon Sep 17 00:00:00 2001 From: Paarth Date: Fri, 25 Dec 2020 22:40:16 -0500 Subject: [PATCH] matcher supports custom key property, refactoring. --- docu/docs/changelog.md | 9 +- src/deprecated.ts | 92 ++++++++++++++++++++ src/index.ts | 9 +- src/lookup.ts | 6 +- src/match.ts | 97 +++++++-------------- src/matcher.ts | 55 +++++++----- src/tools.ts | 14 +-- src/util.ts | 9 +- src/variant.spec.ts | 28 ++++-- src/variant.ts | 188 ++++++++++++++++++++++++++--------------- 10 files changed, 322 insertions(+), 185 deletions(-) create mode 100644 src/deprecated.ts diff --git a/docu/docs/changelog.md b/docu/docs/changelog.md index 4e129fc..7b7994f 100644 --- a/docu/docs/changelog.md +++ b/docu/docs/changelog.md @@ -5,8 +5,9 @@ Summary of the changes in each patch. ## 2.0.3 - Added `matcher()` function - - Added `constrainedVariant()` and `patternedVariant()`. - - Added match helpers `just()` and `unpack()` + - Added `constrained()`, `patterned()`, and `augmented()`. + - Added match helpers `just()` (alias for `constant()`) and `unpack()` + - `outputTypes()` gets a more specific return type. ## 2.0.2 - Added `isType` utility - and a curried overload @@ -18,6 +19,8 @@ Summary of the changes in each patch. - their functionality is now covered by `match` - `variantModule` also accepts `{}` - improved generic handling (`genericVariant`) + - Acknowledgments + - *Thank you [@ohana54](https://github.com/paarthenon/variant/issues/7) for the discussion that led to `isType` and the `match` overloads.* ## 2.0.1 - exposed `variantModule` @@ -27,4 +30,4 @@ Summary of the changes in each patch. - added recursive and generic variants - added `variantModule` - `variantList` now accepts raw string literals - - match \ No newline at end of file + - match gets a helper, `constant()` \ No newline at end of file diff --git a/src/deprecated.ts b/src/deprecated.ts new file mode 100644 index 0000000..5ef7408 --- /dev/null +++ b/src/deprecated.ts @@ -0,0 +1,92 @@ +/** + * THIS FILE is for the content I intent to remove on Variant 3.0 that + * I'm tired of seeing or considering in the code, so it gets shuffled + * off to here. + */ +import {Defined, Handler, match} from './match'; +import {Func, Identity} from './util'; +import {Outputs, Property, VariantModule, VariantsOfUnion} from './variant'; + + +/** + * @deprecated + * A variant with some extra properties attached (use augmented instead) + */ +export type AugmentVariant = { + [P in keyof T]: ((...args: Parameters) => Identity & U>) & Outputs +} + +/** + * @deprecated + * Expand the functionality of a variant as a whole by tacking on properties + * generated by a thunk. + * @param variantDef + * @param f + */ +export function augment(variantDef: T, f: F) { + return Object.keys(variantDef).reduce((acc, key) => { + const augmentedFuncWrapper = (...args: any[]) => (Object.assign({}, f(), variantDef[key](...args))); + return { + ...acc, + [key]: Object.assign(augmentedFuncWrapper, {key: variantDef[key].key, type: variantDef[key].type}) + }; + }, {} as AugmentVariant>); +} + + + +/** + * Built to describe an object with the same keys as a variant but instead of constructors + * for those objects has functions that handle objects of that type. In this case, the + * keys are all partial and there is an extra option "default" + */ +export type DefaultedHandler = Partial & {default?: (union: T[keyof T]) => U}> + +/** + * Match a variant against some of its possible options and do some + * processing based on the type of variant received. May return undefined + * if the variant is not accounted for by the handler. + * @deprecated + * @param obj + * @param handler + * @param typeKey override the property to inspect. By default, 'type'. + */ +export function partialMatch< + T extends Property, + K extends string = 'type' +> ( + obj: T, + handler: DefaultedHandler>, + typeKey?: K, +): ReturnType> { + return match(obj, handler as any, typeKey) as any; +}; + + +/** + * Match a variant against it's some of its possible options and do some + * processing based on the type of variant received. Finally, take the remaining + * possibilities and handle them in a function. + * + * The input to the 'or' clause is well-typed. + * + * @deprecated + * @param obj the variant in question + * @param handler an object whose keys are the type names of the variant's type and values are handler functions for each option. + * @param {string?} typeKey override the property to inspect. By default, 'type'. + * @returns {The union of the return types of the various branches of the handler object} + */ +export function matchElse< + T extends Property, + H extends Partial>>, + E extends (rest: Exclude>) => any, + K extends string = 'type' +> ( + obj: T, + handler: H, + _else: E, + typeKey?: K, +): ReturnType> | ReturnType { + const typeString = obj[typeKey ?? 'type' as K]; + return handler[typeString]?.(obj as any) ?? _else(obj as any) +} diff --git a/src/index.ts b/src/index.ts index 6588e1f..ad515b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,11 +3,12 @@ export { Anonymous, } from './nominal'; +export * from './deprecated'; + export { - augment, - AugmentVariant, + augmented, cast, - constrainedVariant, + constrained, isOfVariant, keymap, KeyMap, @@ -18,6 +19,8 @@ export { Matrix, narrow, outputTypes, + patterned, + Property, TypeExt, TypeNames, variant, diff --git a/src/lookup.ts b/src/lookup.ts index 737aa64..79cffbf 100644 --- a/src/lookup.ts +++ b/src/lookup.ts @@ -1,4 +1,4 @@ -import {WithProperty, VariantsOfUnion} from './variant'; +import {Property, VariantsOfUnion} from './variant'; /** * An object that has the same keys as a variant but has arbitrary values for the data. @@ -15,7 +15,7 @@ export type Lookup = { * @param typeKey the key used as the discriminant. */ export function lookup< - T extends WithProperty, + T extends Property, L extends Lookup>, K extends string = 'type' >(obj: T, handler: L, typeKey?: K): L[keyof L] { @@ -31,7 +31,7 @@ export function lookup< * @param typeKey the key used as the discriminant. */ export function partialLookup< - T extends WithProperty, + T extends Property, L extends Lookup>, K extends string = 'type' >(obj: T, handler: Partial, typeKey?: K): L | undefined { diff --git a/src/match.ts b/src/match.ts index 2e28412..935a5c1 100644 --- a/src/match.ts +++ b/src/match.ts @@ -1,11 +1,7 @@ import {Func} from './util'; -import {TypeExt, UnionHandler, WithProperty, VariantsOfUnion, VariantModule, KeysOf, Variant} from './variant'; +import {TypeExt, UnionHandler, VariantsOfUnion, VariantModule, KeysOf, Variant, Property} from './variant'; -/** - * Strip undefined from a union of types. - */ -type Defined = T extends undefined ? never : T; /** * Built to describe an object with the same keys as a variant but instead of constructors @@ -15,15 +11,24 @@ export type Handler = { [P in keyof T]: (variant: T[P]) => U } +/** + * Either the full handler or the partial set plus 'default' + */ type WithDefault = | Partial & {default: (...args: Parameters[keyof T]>) => U} | T ; +/** + * Pick just the functions of an object. + */ type FuncsOnly = { [P in keyof T]: T[P] extends Func ? T[P] : never; } +/** + * The key used to indicate the default handler. + */ export const DEFAULT_KEY = 'default'; export type DEFAULT_KEY = typeof DEFAULT_KEY; @@ -32,20 +37,31 @@ export type DEFAULT_KEY = typeof DEFAULT_KEY; */ export interface VariantError { __error: never, __message: T }; +/** + * Prevents 'overflow' in a literal. + * + * @todo this may be unnecessary if you use that 'splay partial' trick. + */ export type Limited = Exclude extends never ? T : VariantError<['Expected keys of handler', keyof T, 'to be limited to possible keys', U]> ; +/** + * Strip undefined from a union of types. + */ +export type Defined = T extends undefined ? never : T; + + /** * Match a variant against its possible options and do some processing * based on the type of variant received. * @param obj the variant in question * @param handler an object whose keys are the type names of the variant's type and values are handler functions for each option. - * @returns {The union of the return types of the various branches of the handler object} + * @returns The union of the return types of the various branches of the handler object */ export function match< - T extends WithProperty<'type', string>, + T extends Property<'type', string>, H extends WithDefault>>, >(obj: T, handler: H & Limited): ReturnType, T['type'] | DEFAULT_KEY>[keyof H]>; @@ -55,10 +71,10 @@ export function match< * @param obj the variant in question * @param handler an object whose keys are the type names of the variant's type and values are handler functions for each option. * @param {string?} typeKey override the property to inspect. By default, 'type'. - * @returns {The union of the return types of the various branches of the handler object} + * @returns The union of the return types of the various branches of the handler object */ export function match< -T extends WithProperty, +T extends Property, H extends WithDefault>>, K extends string = 'type' >(obj: T, handler: H & Limited, typeKey?: K): ReturnType, T[K] | DEFAULT_KEY>[keyof H]>; @@ -75,7 +91,7 @@ K extends string = 'type' * @returns {The union of the return types of the various branches of the handler object} */ export function match< - T extends WithProperty, + T extends Property, H extends Partial>>, E extends (rest: Exclude>) => any, K extends string = 'type' @@ -84,7 +100,7 @@ export function match< * Actual impl */ export function match< - T extends WithProperty, + T extends Property, H extends Partial>> | WithDefault>>, E extends (rest: Exclude>) => any, K extends string = 'type' @@ -106,63 +122,6 @@ export function match< } } - -/** - * Built to describe an object with the same keys as a variant but instead of constructors - * for those objects has functions that handle objects of that type. In this case, the - * keys are all partial and there is an extra option "default" - */ -export type DefaultedHandler = Partial & {default?: (union: T[keyof T]) => U}> - -type Keyless = T & {key: never}; -/** - * Match a variant against some of its possible options and do some - * processing based on the type of variant received. May return undefined - * if the variant is not accounted for by the handler. - * @param obj - * @param handler - * @param typeKey override the property to inspect. By default, 'type'. - */ -export function partialMatch< - T extends WithProperty, - K extends string = 'type' -> ( - obj: T, - handler: DefaultedHandler>, - typeKey?: K, -): ReturnType> { - return match(obj, handler as any, typeKey) as any; -}; - - -/** - * Match a variant against it's some of its possible options and do some - * processing based on the type of variant received. Finally, take the remaining - * possibilities and handle them in a function. - * - * The input to the 'or' clause is well-typed. - * - * @param obj the variant in question - * @param handler an object whose keys are the type names of the variant's type and values are handler functions for each option. - * @param {string?} typeKey override the property to inspect. By default, 'type'. - * @returns {The union of the return types of the various branches of the handler object} - */ -export function matchElse< - T extends WithProperty, - H extends Partial>>, - E extends (rest: Exclude>) => any, - K extends string = 'type' -> ( - obj: T, - handler: H, - _else: E, - typeKey?: K, -): ReturnType> | ReturnType { - const typeString = obj[typeKey ?? 'type' as K]; - return handler[typeString]?.(obj as any) ?? _else(obj as any) -} - - /** * Match a literal against some of its possible options and do some processing based * on the type of literal received. Works well with strEnum @@ -173,6 +132,8 @@ export function matchLiteral>(litera return handler[literal]?.(literal); } +// I'm not sure if this is still necessary but I don't want to mess +// with the match types until I add more strict tests. type Limit = { [P in keyof T]: P extends Keys ? T[P] : never; } diff --git a/src/matcher.ts b/src/matcher.ts index dce2190..f2154c4 100644 --- a/src/matcher.ts +++ b/src/matcher.ts @@ -12,36 +12,43 @@ import {KeysOf, TypeExt, Variant, VariantCreator, VariantsOfUnion} from './varia * Finish handling with a terminal: `.complete()`, `.execute()`, or `.else()`. */ export type Matcher< - T extends TypeExt<'type', string>, - H extends Partial>>, + T extends TypeExt, + H extends Partial>>, K extends string = 'type', > = { + /** + * The in-progress handler. + */ readonly handler?: H; /** - * The target object being matched + * The target object being matched. */ readonly target: T; + /** + * The key used as the discriminant. + */ + readonly key: K; /** * Handle one or more cases using a `match`-like handler object. */ - when>>>(hp: HPrime): Matcher; + when>, K>>(hp: HPrime): Matcher; /** * Handle one or more cases by specifying the relevant types in an array * then providing a function to process a variant of one of those types. */ when< - KPrime extends (Exclude | VariantCreator, Func>), - HFunc extends (x: ExtractOfUnion>) => any, + KPrime extends (Exclude | VariantCreator, Func, K>), + HFunc extends (x: ExtractOfUnion, K>) => any, >( keys: KPrime[], handler: HFunc, - ): Matcher, HFunc>, K>; + ): Matcher, HFunc>, K>; /** * Execute the match immediately. Exhaustiveness is not guaranteed, but if the matcher * has missing cases this function may return `undefined`. In that scenario the `undefined` * type will be added to this function's possible return types. */ - execute(): Exclude extends never + execute(): Exclude extends never ? ReturnType> : (ReturnType> | undefined) ; @@ -49,9 +56,9 @@ export type Matcher< * Take any remaining cases and handle them in a single function. This is a terminal and * will execute the match immediately. If a previous `when()` statement */ - else>) => any>(f: F): ReturnType | F>; + else>) => any>(f: F): ReturnType | F>; -} & (Exclude extends never ? { +} & (Exclude extends never ? { /** * Only exists if all cases are handled. */ @@ -59,7 +66,7 @@ export type Matcher< } : {}); -type GetKey)> = T extends string ? T : T extends VariantCreator ? TT : never; +type GetKey), K extends string = 'type'> = T extends string ? T : T extends VariantCreator ? TT : never; type Coalesce = T extends undefined ? U : T; @@ -72,18 +79,20 @@ type EnsureFunc = T extends Func ? T : never; * @param _typeKey */ export function matcher< - T extends TypeExt<'type', string>, + T extends TypeExt, H extends {}, K extends string = 'type', >(target: T, handler = {} as H, _typeKey = 'type' as K): Matcher { return { target, handler, - when: function>>>( + key: _typeKey ?? 'type', + when: function>, K>>( this: Matcher, - hp: HPrime | Array>, + hp: HPrime | Array>, hfunc?: Func ) { + // The case where an array is passed in. if (Array.isArray(hp)) { const keys = hp.map(keyOrVC => { if (typeof keyOrVC === 'string') { @@ -120,14 +129,14 @@ export function matcher< } as Func, execute: function(this: Matcher) { console.log(this.handler); - const result: (x: T) => unknown = (this.handler as any)[target.type] - return result(target); + const result: (x: T) => unknown = (this.handler as any)[this.target[this.key]] + return result(this.target); } as Func, complete: function(this: Matcher) { return this.execute(); } as Func, else: function(this: Matcher, func: (x: T) => unknown) { - if (this.target.type in (this.handler ?? {})) { + if (this.target[this.key] in (this.handler ?? {})) { return this.execute(); } else { return func(this.target); @@ -138,16 +147,16 @@ export function matcher< -type SplayPartial> = - Identity[T['type']]>; +type SplayPartial, K extends string = 'type'> = + Identity[T[K]]>; -type SplayCase, Types extends string> = { - [P in T['type']]: KeyCase) => any>; +type SplayCase,K extends string = 'type'> = { + [P in T[K]]: KeyCase) => any, K>; } /** * */ -type KeyCase, Types extends string, Type extends Types, F extends (...args: any[]) => any> - = Partial>> & TypeExt; +type KeyCase, Types extends string, Type extends Types, F extends (...args: any[]) => any, K extends string = 'type'> + = Partial>> & TypeExt; diff --git a/src/tools.ts b/src/tools.ts index b9694b7..b9b709d 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,4 +1,4 @@ -import variant, {Outputs, Variant, VariantCreator, WithProperty} from "./variant"; +import variant, {Outputs, Property, Variant, VariantCreator} from "./variant"; import {ExtractOfUnion, Func, Identity} from "./util"; /** @@ -69,7 +69,7 @@ export function payload(_example?: T) { } export function property(key: K) { - return () => (payload: T) => ({[key]: payload}) as K extends keyof infer KLiteral ? WithProperty : never; + return () => (payload: T) => ({[key]: payload}) as K extends keyof infer KLiteral ? Property : never; } export function data(x: T) { @@ -91,7 +91,7 @@ type FlattenToTypeStr), K ex * Curried isType, useful for `.filter` or rxjs * @param type */ -export function isType), K extends string = 'type'>(type: T): > (o: O) => o is ExtractOfUnion, K>; +export function isType), K extends string = 'type'>(type: T): > (o: O) => o is ExtractOfUnion, K>; /** * Check if an object is of a given type. The type here * may be a string or a variant constructor (i.e. `Animal.dog`). @@ -105,7 +105,7 @@ export function isType), K e * @param key optional discriminant key override. 'type' by default. */ export function isType< - O extends WithProperty, + O extends Property, T extends (O[K] | VariantCreator), K extends string = 'type', >( @@ -114,7 +114,7 @@ export function isType< key?: K, ): instance is ExtractOfUnion ? R : T extends string ? T : never, K>; export function isType< - O extends WithProperty, + O extends Property, T extends (O[K] | VariantCreator), K extends string = 'type', >( @@ -126,13 +126,13 @@ export function isType< if (typeof instanceOrType === 'function' || typeof instanceOrType === 'string') { const typeArg = instanceOrType as T; const typeStr = typeof typeArg === 'string' ? typeArg : (typeArg as VariantCreator).type; - return >(o: O): o is ExtractOfUnion, K> => isType(o, typeStr); + return >(o: O): o is ExtractOfUnion, K> => isType(o, typeStr); } else { const instance = instanceOrType as O; const type = typeOrKey as T; const typeStr = typeof type === 'string' ? type : (type as VariantCreator).type; - return instance != undefined && (instance as WithProperty)[key ?? 'type'] === typeStr; + return instance != undefined && (instance as Property)[key ?? 'type'] === typeStr; } } else { return false; diff --git a/src/util.ts b/src/util.ts index d44dc50..674968b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,4 @@ -import {WithProperty, VariantModule} from './variant'; +import {VariantModule, Property} from './variant'; /** * Useful in generating friendly types. Intersections are rendered as the type of the intersection, not as A & B. @@ -7,11 +7,10 @@ export type Identity = {} & { [P in keyof T]: T[P] }; -export const identityFunc = (x?: T) => (x || {}) as T extends unknown ? {} : T ; +export const identityFunc = (x = {} as T) => x as T extends unknown ? {} : T ; export type Func = (...args: any[]) => any; - export interface FuncObject { [key: string]: Func } @@ -22,7 +21,7 @@ export interface FuncObject { */ export type GetDataType, K extends string = 'type'> = { [P in keyof T]: ReturnType extends PromiseLike - ? R extends WithProperty ? R : never + ? R extends Property ? R : never : ReturnType } @@ -30,7 +29,7 @@ export type GetDataType, K extends string = 'type'> = * Given a union of types all of which meet the contract {[K]: string} * extract the type that is specifically {[K]: TType} */ -export type ExtractOfUnion = T extends WithProperty ? T : never; +export type ExtractOfUnion = T extends Property ? T : never; /** * Utility function to create a K:V from a list of strings diff --git a/src/variant.spec.ts b/src/variant.spec.ts index 26593b9..4223fc2 100644 --- a/src/variant.spec.ts +++ b/src/variant.spec.ts @@ -24,7 +24,7 @@ import { payload, just, } from './index'; -import {constrainedVariant, keylist, patternedVariant} from './variant'; +import {augmented, constrained, flags, patterned} from './variant'; import {Animal, cerberus} from './__test__/animal'; test('empty variant', () => { @@ -83,6 +83,21 @@ test('augment', () => { expect(snek.better).toBe(true); }) +test('augmented', () => { + const BetterAnimal = variantModule(augmented(() => ({better: 4}),{ + dog: fields<{name: string, favoriteBall?: string}>(), + cat: fields<{name: string, furnitureDamaged: number}>(), + snake: (name: string, pattern = 'striped') => ({name, pattern}), + })); + type BetterAnimal = undefined> = VariantOf; + + + const snek = BetterAnimal.snake('steve'); + expect(snek.name).toBe('steve'); + expect(snek.better).toBeDefined(); + expect(snek.better).toBe(4); +}) + test('cast', () => { const animal = Animal.snake('Steven') as any as Animal; @@ -168,7 +183,6 @@ test('keymap', () => { type Anim = undefined> = VariantOf; const thing = keymap(Anim); - const list = keylist(Anim); type asdf = KeyMap; thing.dog; }) @@ -375,9 +389,9 @@ test('Generic (maybe)', () => { test('constrained', () => { - const Test1 = constrainedVariant((_x: string) => ({min: 4}), { + const Test1 = variantModule(constrained((_x: string) => ({min: 4}), { Yo: (_x: string, min: number) => ({min}), - }); + })); type Test1 = undefined> = VariantOf; const instance = Test1.Yo('hello', 4); @@ -396,12 +410,12 @@ test('constrained 2', () => { BackLength, } - const HairStyle = constrainedVariant(just<{min?: HairLength, max?: HairLength}>({}), { + const HairStyle = variantModule(constrained(just<{min?: HairLength, max?: HairLength}>({}), { Bald: just({max: HairLength.Bald}), Pixie: just({min: HairLength.Short, max: HairLength.Medium}), Straight: just({min: HairLength.Short}), Waves: just({min: HairLength.Medium}), - }); + })); type HairStyle = undefined> = VariantOf; const baldie = HairStyle.Bald() as HairStyle; @@ -425,4 +439,4 @@ test('constrained 2', () => { // expect(rating(Animal2.Cat({name: 'steve'}))).toBe('steve'); // }); -// test('scopedMatch') \ No newline at end of file +// test('scopedMatch') diff --git a/src/variant.ts b/src/variant.ts index 06922b2..60c5c53 100644 --- a/src/variant.ts +++ b/src/variant.ts @@ -1,9 +1,20 @@ import variantDefault from '.'; import {Identity, Func, identityFunc, GetDataType, ExtractOfUnion, strEnum, isPromise} from './util'; -// Consider calling this ObjectEntry or Entry. Also Pair? No, more like KVPair. Mapping? -export type TypeExt = K extends keyof infer LitK ? {[P in keyof LitK]: T} : never; -export type WithProperty = TypeExt; +/** + * A type representing an object with a single property. + * - `Property` evaluates to `{ [K: string]: T }` + */ +export type Property = K extends keyof infer LitK ? {[P in keyof LitK]: T} : never; +/** + * @deprecated + * Alias of `Property` + */ +export type WithProperty = Property; +/** + * Alias of `Property` + */ +export type TypeExt = Property; /** * The type marking metadata. It's useful to know the type of the items the function will generate. @@ -13,6 +24,9 @@ export type WithProperty = TypeExt; * prefer to use [Animal.dog.type]: rather than dog: . */ export type Outputs = { + /** + * Discriminant property key + */ key: K /** * The type of object created by this function. @@ -28,27 +42,47 @@ export type Stringable = { } /** - * The constructor for one tag of a variant type. + * The constructor for one tag of a variant type. + * + * - `T` extends `string` — The literal string used as the type + * - `F` extends `(...args: any[]) => {}` = `() => {}` — The function that serves as the variant's *body definition* + * - `K` extends `string` = `'type'` — The discriminant */ export type VariantCreator< T extends string, F extends (...args: any[]) => {} = () => {}, K extends string = 'type'> -= Stringable & ((...args: Parameters) => PatchObjectOrPromise, WithProperty>) & Outputs; -export type PatchObjectOrPromise, U extends {}> = T extends PromiseLike ? PromiseLike> : Identity; += ((...args: Parameters) => PatchObjectOrPromise, Property>) + & Outputs + & Stringable +; /** - * The overall module of variants. This is equivalent to a polymorphic variant. + * Given an object or a promise containing an object, patch it to + * include some extra properties. + * + * This is mostly used to merge the `{type: ______}` property into + * the body definition of a variant. + */ +export type PatchObjectOrPromise< + T extends {} | PromiseLike<{}>, + U extends {} +> = T extends PromiseLike + ? PromiseLike> + : Identity +; + +/** + * A variant module definition. Literally an object to group together + * a set of variant constructors. */ export type VariantModule = { [name: string]: VariantCreator any, K> } -// validator ? - -type EnforceEmpty = keyof T extends never ? {} : never; /** + * Create the `variant` function set to a new discriminant. * Use this function to generate a version of the `variant` factory function using * some arbitrary key. The default `variant` is just `variantFactory('type')`. * @param key The name of the property to use e.g. 'type', 'kind', 'version' @@ -107,9 +141,17 @@ export type Creators, K extends string = 'type'> = { /** * Used in writing cases of a type-first variant. + * + * `Variant<'One', {a: number, b: string}>>` generates + * - `{type: 'One', a: number, b: string}` + * + * You may write the literals directly, using this is recommended + * if you'd like to update the literal as this library updates. */ -export type Variant - = WithProperty & Fields; +export type Variant< + Type extends string, Fields extends {} = {}, + Key extends string = 'type', +> = Property & Fields; type InternalVariantsOf, K extends string ='type'> = GetDataType, K>; @@ -166,17 +208,14 @@ export type PatternedRawVariant = {[type: string]: F} * Patched Constrained Raw Variant */ type PatchedCRV, F extends Func> = { - [P in keyof T]: (...args: Parameters) => ReturnType & ReturnType; -} -/** - * Patched Constrained Raw Variant - */ -type PatchedPRV, F extends Func> = { - [P in keyof T]: (...args: Parameters) => ReturnType & ReturnType; + [P in keyof T]: (...args: Parameters) => Identity & ReturnType>; } type CleanResult = T extends undefined ? U : T extends Func ? T : T extends object ? U : T; +type FullyFuncRawVariant = { + [P in keyof V & string]: CleanResult {}> +} export type OutVariant = {[P in (keyof T & string)]: VariantCreator {}>>} ; @@ -198,40 +237,69 @@ export function variantModule< }, {} as OutVariant); } -/** - * Unstable. - * @param v - * @param _contract - */ -export function constrainedVariant< +export function constrained< T extends ConstrainedRawVariant, F extends Func, ->(_contract: F, v: T): Identity>> { - return safeKeys(v).reduce((acc, key) => { +>(_constraint_: F, v: T) { + return v as PatchedCRV; +} +export function patterned< + T extends PatternedRawVariant, + F extends Func, +>(_constraint_: F, v: T) { + return v as PatchedCRV; +} + +/** + * Take a variant, including some potential `{}` cases + * and generate an object with those replaced with the `noop` function. + */ +function funcifyRawVariant(v: V) { + return safeKeys(v).reduce((acc, cur) => { return { ...acc, - [key]: variant(key, typeof v[key] === 'function' ? v[key] as any : identityFunc), - }; - }, {} as OutVariant); + [cur]: typeof v[cur] === 'function' ? v[cur] : () => {}, + } + }, {}) as FullyFuncRawVariant +} + +export type AugmentedRawVariant = { + [P in keyof V & string]: (...args: Parameters[P]>) => (ReturnType & ReturnType[P]>) } /** - * Unstable. - * @param v - * @param _contract + * Expand the functionality of a variant as a whole by tacking on properties + * generated by a thunk. + * + * Used in conjunction with `variantModule` + * + * ```typescript + * export const Action = variantModule(augmented( + * () => ({created: Date.now()}), + * { + * AddTodo: fields<{text: string, due?: number}>(), + * UpdateTodo: fields<{todoId: number, text?: string, due?: number, complete?: boolean}>(), + * }, + * )); + * ``` + * @param variantDef + * @param f */ -export function patternedVariant< - T extends PatternedRawVariant, - F extends Func, ->(_contract: F, v: T): Identity>> { - return safeKeys(v).reduce((acc, key) => { +export function augmented) => any>(f: F, variantDef: T) { + const funkyDef = funcifyRawVariant(variantDef); + return safeKeys(funkyDef).reduce((acc, key) => { return { ...acc, - [key]: variant(key, typeof v[key] === 'function' ? v[key] as any : identityFunc), - }; - }, {} as OutVariant); + [key]: (...args: Parameters[typeof key]>) => { + const item = funkyDef[key](...args); + return { + ...f(item), + ...item, + } + } + } + }, {}) as AugmentedRawVariant; } - // WAIT UNTIL VARIANT 3.0 FOR TYPESCRIPT 4.1 FEATURES // // type ScopedVariant = { @@ -257,12 +325,19 @@ export function patternedVariant< /** + * Get a list of types a given module will support. + * + * These are the concrete types, not the friendly keys in the module. + * This is mostly used internally to check whether or not a message is of + * a given variant (`outputTypes(Animal).includes(x.type)`) * Give an array of output types for a given variant collection. * Useful for checking whether or not a message belongs in your * variant set at runtime. * @param variantObject */ -export function outputTypes}>(variantObject: T): T[keyof T]['type'][] { +export function outputTypes< + T extends {[name: string]: Outputs} +>(variantObject: T): T[keyof T]['type'][] { return Object.keys(variantObject).map(key => variantObject[key].type); } @@ -316,21 +391,17 @@ export type UnionHandler = { /** * From a given union type, extract the the variant object's type. */ -export type VariantsOfUnion, K extends string = 'type'> = { +export type VariantsOfUnion, K extends string = 'type'> = { [P in T[K]]: ExtractOfUnion } -export type AugmentVariant = { - [P in keyof T]: ((...args: Parameters) => Identity & U>) & Outputs -} - /** * Set a variable's type to a new case of the same variant. * @param obj object of concern. * @param _type new type tag. Restricted to keys of the variant. * @param _typeKey discriminant key. */ -export function cast, T extends O[K], K extends string = 'type'>(obj: O, _type: T, _typeKey?: K) { +export function cast, T extends O[K], K extends string = 'type'>(obj: O, _type: T, _typeKey?: K) { return obj as ExtractOfUnion; } /** @@ -339,26 +410,11 @@ export function cast, T extends O[K], K extend * @param type new type. Restricted to keys of the variant. * @param typeKey discriminant key. */ -export function narrow, T extends O[K], K extends string = 'type'>(obj: O, type: T, typeKey?: K) { +export function narrow, T extends O[K], K extends string = 'type'>(obj: O, type: T, typeKey?: K) { const typeString = obj[typeKey ?? 'type' as K]; return typeString === type ? obj as ExtractOfUnion : undefined; } -/** - * Expand the functionality of a variant as a whole by tacking on properties - * generated by a thunk. - * @param variantDef - * @param f - */ -export function augment(variantDef: T, f: F) { - return Object.keys(variantDef).reduce((acc, key) => { - const augmentedFuncWrapper = (...args: any[]) => (Object.assign({}, f(), variantDef[key](...args))); - return { - ...acc, - [key]: Object.assign(augmentedFuncWrapper, {key: variantDef[key].key, type: variantDef[key].type}) - }; - }, {} as AugmentVariant>); -} export type SumType, K extends string = 'type'> = InternalVariantsOf[keyof T]; export type KeyMap, K extends string = 'type'> = { @@ -420,7 +476,7 @@ export type Flags = Partial>; * @param flags * @param typeKey */ -export function flags, K extends string = 'type'>(flags: T[], typeKey?: K): {[P in T[K]]: ExtractOfUnion} { +export function flags, K extends string = 'type'>(flags: T[], typeKey?: K): {[P in T[K]]: ExtractOfUnion} { return flags.reduce((o, v) => ({ ...o, [v[typeKey ?? 'type']]: v,