variant
Version:
Variant types (a.k.a. Discriminated Unions) in TypeScript
244 lines • 8.97 kB
JavaScript
;
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