Last active
September 19, 2019 18:13
-
-
Save flisboac/7d1e94eaaea3629a81f0bd1a063af0b3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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