Skip to content

Instantly share code, notes, and snippets.

@flisboac
Last active September 19, 2019 18:13
Show Gist options
  • Save flisboac/7d1e94eaaea3629a81f0bd1a063af0b3 to your computer and use it in GitHub Desktop.
Save flisboac/7d1e94eaaea3629a81f0bd1a063af0b3 to your computer and use it in GitHub Desktop.
/**
* Models a type guard function.
*/
export interface Guard<T> {
readonly name: string;
(value: any): value is T;
}
/**
* Models a factory for cast wrappers.
*/
export interface MaybeCastFactory<T> {
<R extends T>(value: R): JustCast<R>;
(value: T): JustCast<T>;
(value: any): MaybeCast<T>;
}
/**
* Models a function that throws a custom exception whenever an
* invalid cast is performed.
*/
export interface CastThrower<T> {
(value: any, info: CastInfo<T>): never;
}
/**
* Models a function that consumes the value contained in a cast wrapper.
*/
export interface CastConsumer<T> {
(value: T, info: CastInfo<T>): any;
}
/**
* Models a function that may transform a value contained in a cast wrapper into
* a value of a type that is strictly a subclass of the guarded type.
*/
export interface CastGetter<T, R extends T = T> {
(value: T, info: CastInfo<T>): R;
}
/**
* Models a function that may transform a value contained in a cast wrapper into
* a value of a type that can possibly be different from the guarded type.
*/
export interface CastMapper<T, R> {
(value: T, info: CastInfo<T>): R;
}
/**
* Models a function that tests for some property of a cast wrapper's value.
*/
export interface CastTester<T, B extends boolean = boolean> {
(value: T, info: CastInfo<T>): B;
}
/**
* Models a function that wraps a cast wrapper's value into some other cast wrapper,
* whilst guaranteeing that the new wrapper's type is a subtype of the original guarded type.
*/
export interface CastFlatGetter<T, R extends T, M extends MaybeCast<R> = MaybeCast<R>> {
(value: T, info: CastInfo<T>): M;
}
/**
* Models a function that wraps a cast wrapper's value into some other cast wrapper.
*/
export interface CastFlatMapper<T, R, M extends MaybeCast<R> = MaybeCast<R>> {
(value: T, info: CastInfo<T>): M;
}
/**
* The error thrown by cast wrappers by default whenever an invalid cast is performed.
*/
export class TypeCastError<T = any> extends TypeError {
constructor(message: string, public readonly cast: MaybeCast<T>) {
super(message);
}
}
export interface CastInfo<T> {
/**
* Contains the guard used to protect access and/or perform type casting.
*
* Present as a form of type information, so that user callbacks are
* able to take action based on the initial type information.
* May not be present when chaining wrappers (i.e. `or` or `flat` methods).
*/
readonly guard?: Guard<T>;
/**
* Indicates whether this cast wrapper is assigned or not.
*/
readonly assigned: boolean;
/**
* Contains the (unprotected) casted value.
*
* Access to `value` is effectively unprotected. It's advised to condition
* such access with `assigned === true`, or to use one of the `get` (i.e. guarded)
* methods. If there is any need to use this value as part of any block of
* code expecting a value of the correct type, please verify if the monad is
* assigned first.
*/
readonly value?: any;
}
interface MaybeCast<T> extends CastInfo<T> {}
abstract class MaybeCast<T> implements CastInfo<T> {
/** Creates a new empty, non-casted, cast wrapper with no type information. */
static of<T = never>(): NoneCast<T>;
/** Creates a new non-empty, non-casted, cast wrapper with no type information. */
static of<T>(value: T): JustCast<T>;
/**
* Tries to cast a value and returns a new cast wrapper with
* type information.
*
* The resulting wrapper will be non-empty only if the value passed
* passes the guard function's test.
*/
static of<T>(value: any, guard: Guard<T>): MaybeCast<T>;
static of<T>(...args: any[]): MaybeCast<T> {
if (args.length === 0) return noneCast;
if (args.length === 1) return new JustCast<T>(args[0]);
const value = args[0];
const guard: Guard<T> = args[1];
if (guard(value)) return new JustCast<T>(value);
return new NoneCast<T>(value, guard);
}
abstract readonly value: any;
/**
* Returns the casted value, or `fallback` if the wrapper is empty.
*/
abstract orElse<R extends T>(fallback: R): T | R;
/**
* Returns the casteed value, or the result of a call to `getter` if the
* wrapper is empty.
*
* The casted value is always passed as an argument to the `getter` call.
* Contrary to `orMap`, this call maintains the initial base type; any
* attempts to change this base type will result in compile-time errors.
*/
abstract orGet<R extends T>(getter: CastGetter<T, R>): T | R;
/**
* Returns the casted value, or the result of a call to `mapper` if
* the wrapper is empty.
*
* The casted value is always passed as an argument to the `mapper` call;
* much like `Array.map`, the `mapper` passed to `MaybeCast.orMap` is
* free to return a value of a completely unrelated type if so desired
* (i.e. in a different type).
*/
abstract orMap<R>(mapper: CastMapper<T, R>): T | R;
/**
* Returns the casted value, or throws an exception if the wrapper is empty.
*
* An exception thrower function may be passed, which will be called
* if `value` cannot be casted.
*
* Optionally, a message may be passed instead, which will be the error
* message of a `TypeCastError` exception if `value` cannot be casted.
*/
abstract orThrow(thrower?: CastThrower<T> | string): T;
/** Returns `this` if not empty, or `fallback` otherwise.
* Type information (e.g. guard) is propagated.
*/
abstract or<R extends T, M extends MaybeCast<R> = MaybeCast<R>>(fallback: M): JustCast<T> | M;
/** If not empty, returns `this`; otherwise, returns a new non-empty wrapper
* with `value` otherwise. Type information (e.g. guard) is propagated.
*/
abstract orJust<R extends T>(value: R): JustCast<T> | JustCast<R>;
/** If not empty, returns `this`; otherwise, returns a new non-empty wrapper
* with the result of a call to `getter`. Type information (e.g. guard) is
* propagated.
*/
abstract orJustGet<R extends T>(value: CastGetter<any, R>): JustCast<T> | JustCast<R>;
/** If not empty, returns `this`; otherwise, returns a new non-empty wrapper
* with the result of a call to `mapper`. Type information (e.g. guard) is
* expected to be lost.
* (This only exists for completeness... Perhaps it's not needed.)
*/
abstract orJustMap<R>(mapper: CastMapper<any, R>): JustCast<T> | JustCast<R>;
/** If not empty, returns `this`; otherwise, returns a wrapper provided by
* calling `maybeGetter`. Type information (e.g. guard) is expected to be lost.
*/
abstract orFlatGet<R extends T>(maybeGetter: CastFlatGetter<any, R>): JustCast<T> | MaybeCast<R>;
/** If not empty, returns `this`; otherwise, returns a wrapper provided by
* calling `maybeMapper`. Type information (e.g. guard) is expected to be lost.
*/
abstract orFlatMap<R>(maybeMapper: CastFlatMapper<any, R>): JustCast<T> | MaybeCast<R>;
/** If not empty, returns `this`; otherwise, throws an exception. */
abstract just(thrower?: CastThrower<T> | string): JustCast<T>;
/** If not empty, executes `consumer` with the wrapped value; otherwise, does nothing. */
abstract ifAssigned(consumer: CastConsumer<T>): void;
abstract get<R extends T>(transformer: CastGetter<T, R>): R;
abstract get<R extends T = T>(transformer?: CastGetter<T, R>): T | R;
abstract get(): T;
abstract map<R>(mapper: CastMapper<T, R>): R;
abstract some(tester: CastTester<T>): boolean;
abstract filter(tester: CastTester<T>): T | undefined;
abstract forEach(consumer: CastConsumer<T>): void;
abstract flatGet<R extends T>(getter: CastFlatGetter<T, R>): MaybeCast<R> | NoneCast<T>;
abstract flatMap<R>(mapper: CastFlatMapper<T, R>): MaybeCast<R> | NoneCast<T>;
abstract refinedBy(tester: CastTester<T>): MaybeCast<T>;
/**
* Re-casts the wrapper's value into some other type. If the wrapper is empty, an
* exception is thrown.
*
* `thrower` is used to throw an exception in case `this` is empty. To check for
* emptiness for the newly casted value, use e.g. the idiom
* `this.as(guard).orThrow()` instead.
*
* Effectively, only possible with `interface`-based (e.g. `object`) types.
* Not so useful for class hierarchy, but may help with (type) pattern matching,
* e.g. if a guard checks for a valid interface instead of a valid base constructor.
*/
abstract as<R>(guard: Guard<R>, thrower?: CastThrower<T> | string): MaybeCast<R>;
/** An alias for `get(transformer)`. */
cast<R extends T>(transformer: CastGetter<T, R>): R;
/** An alias for `get(transformer?)`. */
cast<R extends T = T>(transformer?: CastGetter<T, R>): T | R;
/** An alias for `get()`. */
cast(): T;
cast<R extends T>(transformer?: CastGetter<T, R>) {
return this.get(transformer);
}
}
class JustCast<T> extends MaybeCast<T> {
public readonly assigned: true = true;
constructor(
public readonly value: any,
public readonly guard?: Guard<T>,
) {
super();
}
orElse<R extends T>(fallback: R): T { return this.value; }
orGet<R extends T>(getter: CastGetter<T, R>): T { return this.value; }
orMap<R>(mapper: CastMapper<T, R>): T { return this.value; }
orThrow(thrower?: CastThrower<T> | string): T { return this.value; }
or<R extends T, M extends MaybeCast<R> = MaybeCast<R>>(fallback: M): JustCast<T> { return this; }
orJust<R extends T>(value: R): JustCast<T> { return this; }
orJustGet<R extends T>(value: CastGetter<any, R>): JustCast<T> { return this; }
orJustMap<R>(value: CastMapper<any, R>): JustCast<T> { return this; }
orFlatGet(maybe: any): JustCast<T> { return this; }
orFlatMap(maybe: any): JustCast<T> { return this; }
just(thrower?: CastThrower<T> | string): JustCast<T> { return this; }
ifAssigned(consumer: CastConsumer<T>): void { consumer(this.value, this); }
get<R extends T>(transformer: CastGetter<T, R>): R;
get<R extends T = T>(transformer?: CastGetter<T, R>): T | R;
get(): T;
get<R extends T = T>(transformer?: CastGetter<T, R>): T | R {
if (transformer) return transformer(this.value, this);
return this.value;
}
map<R>(mapper: CastMapper<T, R>): R { return mapper(this.value, this); }
some(tester: CastTester<T>): boolean { return tester(this.value, this); }
filter(tester: CastTester<T>): T | undefined { return tester(this.value, this) ? this.value : undefined; }
forEach(consumer: CastConsumer<T>): void { consumer(this.value, this); }
flatGet<R extends T, M extends MaybeCast<R>>(flatGetter: CastFlatGetter<T, R, M>): M { return flatGetter(this.value, this); }
flatMap<R, M extends MaybeCast<R> = MaybeCast<R>>(flatMapper: CastFlatMapper<T, R, M>): M { return flatMapper(this.value, this); }
refinedBy(tester: CastTester<T>): MaybeCast<T> { return tester(this.value, this) ? this : new NoneCast<T>(this.value, this.guard); }
as<R>(guard: Guard<R>, thrower?: CastThrower<T> | string): MaybeCast<R> { return MaybeCast.of(this.value, guard); }
}
class NoneCast<T = any> extends MaybeCast<T> {
public readonly assigned: false = false;
constructor(
public readonly value: any,
public readonly guard?: Guard<T>,
) {
super();
}
orElse<R extends T>(fallback: R): R { return fallback; }
orGet<R extends T>(getter: CastGetter<T, R>): R { return getter(this.value, this); }
orMap<R>(mapper: CastMapper<T, R>): R { return mapper(this.value, this); }
orThrow(thrower?: CastThrower<T> | string): never {
if (thrower instanceof Function) thrower(this.value, this);
else if (thrower) throw new TypeCastError<T>(thrower, this);
if (this.guard) throw new TypeCastError<T>(`Value does not conform to type guard "${this.guard.name}".`, this);
throw new TypeCastError<T>(`Cannot get a value from an empty monad.`, this);
}
or<R extends T, M extends MaybeCast<R>>(fallback: M): M { return fallback; }
orJust<R extends T>(value: R): JustCast<R> { return MaybeCast.of(value); }
orJustGet<R extends T>(getter: CastGetter<any, R>): JustCast<R> { return MaybeCast.of(getter(this.value, this)); }
orJustMap<R>(mapper: CastMapper<any, R>): JustCast<R> { return MaybeCast.of(mapper(this.value, this)); }
orFlatGet<R extends T, M extends MaybeCast<R> = MaybeCast<R>>(maybe: CastFlatGetter<any, R, M>): M { return maybe(this.value, this); }
orFlatMap<R, M extends MaybeCast<R> = MaybeCast<R>>(maybe: CastFlatMapper<any, R, M>): M { return maybe(this.value, this); }
just(thrower?: CastThrower<T> | string): never { this.orThrow(thrower); throw new Error(); }
ifAssigned(consumer: CastConsumer<T>): void { }
get<R extends T>(transformer: CastGetter<T, R>): never;
get<R extends T = T>(transformer?: CastGetter<T, R>): never;
get(): never;
get(transformer?: any): never { this.orThrow(); throw new Error(); }
map<R>(mapper: CastMapper<T, R>): never { this.orThrow(); throw new Error(); }
some(tester: CastTester<T>): never { this.orThrow(); throw new Error(); }
filter(tester: CastTester<T>): never { this.orThrow(); throw new Error(); }
forEach(consumer: CastConsumer<T>): void { this.orThrow(); throw new Error(); }
flatGet<R extends T, M extends MaybeCast<R> = MaybeCast<R>>(flatGetter: CastFlatGetter<T, R, M>): NoneCast<T> { return this; }
flatMap<R, M extends MaybeCast<R> = MaybeCast<R>>(flatMapper: CastFlatMapper<T, R, M>): NoneCast<T> { return this; }
refinedBy(tester: CastTester<T>): NoneCast<T> { return this; }
as<R>(guard: Guard<R>, thrower?: CastThrower<T> | string): never { this.orThrow(thrower); throw new Error(); }
}
export function just<T>(value: T): JustCast<T> {
return new JustCast<T>(value);
}
export function none<T = never>(): NoneCast<T> {
return noneCast;
}
export function castIf<T>(guard: Guard<T>): MaybeCastFactory<T>;
export function castIf<T>(guard: Guard<T>, value: any): MaybeCast<T>;
export function castIf<T>(...args: any[]): MaybeCast<T> | MaybeCastFactory<T> {
const guard: Guard<T> = args[0];
if (args.length > 1) {
const value: any = args[1];
return MaybeCast.of(value, guard);
}
const factory = (value: any) => MaybeCast.of(value, guard);
return factory as any; // duck won't understand
}
export function isCastLike<T = any>(value: any): value is MaybeCast<T> {
return value instanceof MaybeCast;
}
export function isJustCast<T = any>(value: any): value is JustCast<T> {
return value instanceof JustCast;
}
export function isNoneCast<T = any>(value: any): value is NoneCast<T> {
return value instanceof NoneCast;
}
const noneCast = new NoneCast<any>(undefined);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment