UNPKG

variant

Version:

Variant types (a.k.a. Discriminated Unions) in TypeScript

244 lines 8.97 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.flags = exports.keymap = exports.keys = exports.narrow = exports.cast = exports.isOfVariant = exports.types = exports.outputTypes = exports.augmented = exports.patterned = exports.constrained = exports.variantModule = exports.variantList = exports.variant = exports.variantFactory = void 0; const util_1 = require("./util"); /** * 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' */ function variantFactory(key) { function variantFunc(tag, func) { let maker = (...args) => { const returned = (func !== null && func !== void 0 ? func : util_1.identityFunc)(...args); if (util_1.isPromise(returned)) { return returned.then(result => (Object.assign({ [key]: tag }, result))); } else { return Object.assign({ [key]: tag }, returned); } }; const outputs = { key, type: tag, }; return Object.assign(maker, outputs, { toString: function () { return this.type; } }); } // the `variant()` function advertises the key it will use. // this has been updated to just use "key" and "type" but // this is quietly retained for legacy support. const outputKey = { outputKey: key }; return Object.assign(variantFunc, outputKey); } exports.variantFactory = variantFactory; exports.variant = variantFactory('type'); exports.default = exports.variant; /** * Create a variant module based on a list of variants. * * @remarks * Best way to create groups of pre-existing variants. * * @param variants a list of variant creators and `string`s for tags that have no body */ function variantList(variants) { return variants .map((v) => { if (typeof v === 'string') { return exports.variant(v); } else { return v; } }) .reduce((o, v) => (Object.assign(Object.assign({}, o), { [v.type]: v })), Object.create(null)); } exports.variantList = variantList; function safeKeys(o) { return Object.keys(o); } /** * Create a variant module from an object describing the variant's structure. * Each key of the object is a case of the variant. Each value of the object * is the constructor function associated with that key. * @param v */ function variantModule(v) { return safeKeys(v).reduce((acc, key) => { return Object.assign(Object.assign({}, acc), { [key]: exports.variant(key, typeof v[key] === 'function' ? v[key] : util_1.identityFunc) }); }, {}); } exports.variantModule = variantModule; function constrained(_constraint_, v) { return v; } exports.constrained = constrained; function patterned(_constraint_, v) { return v; } exports.patterned = patterned; /** * Take a variant, including some potential `{}` cases * and generate an object with those replaced with the `noop` function. */ function funcifyRawVariant(v) { return safeKeys(v).reduce((acc, cur) => { return Object.assign(Object.assign({}, acc), { [cur]: typeof v[cur] === 'function' ? v[cur] : () => { } }); }, {}); } /** * 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 */ function augmented(f, variantDef) { const funkyDef = funcifyRawVariant(variantDef); return safeKeys(funkyDef).reduce((acc, key) => { return Object.assign(Object.assign({}, acc), { [key]: (...args) => { const item = funkyDef[key](...args); return Object.assign(Object.assign({}, f(item)), item); } }); }, {}); } exports.augmented = augmented; // WAIT UNTIL VARIANT 2.1 FOR TYPESCRIPT 4.1 FEATURES // // type ScopedVariant<T extends RawVariant, Scope extends string> = { // [P in (keyof T & string)]: VariantCreator<`${Scope}__${P}`, CleanResult<T[P], () => {}>>; // } // /** // * Unstable. // * @param v // * @param _contract // */ // export function scopedVariant< // T extends RawVariant, // Scope extends string, // >(scope: Scope, v: T): Identity<ScopedVariant<T, Scope>> { // return safeKeys(v).reduce((acc, key) => { // return { // ...acc, // [key]: variant(`${scope}__${key}`, typeof v[key] === 'function' ? v[key] as any : identityFunc), // }; // }, {} as ScopedVariant<T, Scope>); // } /** * 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 */ function outputTypes(variantObject) { return Object.keys(variantObject).map(key => variantObject[key].type); } exports.outputTypes = outputTypes; function types(content, key) { const typeStr = key !== null && key !== void 0 ? key : 'type'; if (Array.isArray(content)) { return content.map(c => c[typeStr]); } else { return Object.values(content).map(c => c.type); } } exports.types = types; /** * Checks if an object was created from one of a set of variants. This function is a * [user-defined type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) * so TypeScript will narrow the type of `object` correctly. * * @remarks * The variant module may be a pre-existing module or one constructed on the fly. * * @param instance an instance of a variant. * @param variant {T extends VariantModule<K>} the variant module. * @param typeKey the key used as the discriminant. * * @returns instance is variant */ function isOfVariant(instance, variant, typeKey) { return instance != undefined && outputTypes(variant).some(type => type === instance[typeKey !== null && typeKey !== void 0 ? typeKey : 'type']); } exports.isOfVariant = isOfVariant; /** * Unused at the moment. Intended to develop the idea of an "ordered" variant. * @param variants */ function progression(variants) { return variants.reduce((o, v) => (Object.assign(Object.assign({}, o), { [v.type]: v })), Object.create(null)); } /** * 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. */ function cast(obj, _type, _typeKey) { return obj; } exports.cast = cast; /** * * @param obj object of concern. * @param type new type. Restricted to keys of the variant. * @param typeKey discriminant key. */ function narrow(obj, type, typeKey) { const typeString = obj[typeKey !== null && typeKey !== void 0 ? typeKey : 'type']; return typeString === type ? obj : undefined; } exports.narrow = narrow; /** * Return an object cache (`{[P]: P}`) of the keys. * * An object cache is more useful than an array because you can do * constant time checks and you can still reduce to a well-typed * array with Object.keys * @param variantDef */ function keys(variantDef) { return util_1.strEnum(outputTypes(variantDef)); } exports.keys = keys; /** * A variant module does not *necessarily* have a 1-1 mapping from * the key used to refer to the object (Animal.bird) and the key generated * by the variant (ANIMAL_BIRD, @animal/bird, etc.). * @param v */ function keymap(v) { return Object.keys(v).reduce((acc, key) => { return Object.assign(Object.assign({}, acc), { [key]: v[key].type }); }, {}); } exports.keymap = keymap; /** * groupBy list of instances on discriminant key. Assumes unique instance per type. * @param flags * @param typeKey */ function flags(flags, typeKey) { return flags.reduce((o, v) => (Object.assign(Object.assign({}, o), { [v[typeKey !== null && typeKey !== void 0 ? typeKey : 'type']]: v })), Object.create(null)); } exports.flags = flags; //# sourceMappingURL=variant.js.map