Skip to content

Instantly share code, notes, and snippets.

@vagarenko
Last active November 6, 2022 06:50
Show Gist options
  • Save vagarenko/6ad5a81c2b2e7523b26be3737280b2f8 to your computer and use it in GitHub Desktop.
Save vagarenko/6ad5a81c2b2e7523b26be3737280b2f8 to your computer and use it in GitHub Desktop.
Maybe type in Typescript.
/**
* The Maybe type encapsulates an optional value.
*/
export class Maybe<T> {
/** Create an empty value. */
static nothing<T>(): Maybe<T> {
return new Maybe<T>(undefined);
}
/** Create a non-empty value. */
static just<T>(value: T): Maybe<T> {
return new Maybe<T>(value);
}
/** Wrapper for String.match */
static matchString(string: string, regexp: string | RegExp): Maybe<RegExpMatchArray> {
return new Maybe(string.match(<any>regexp));
}
/** Put a `value` into `Maybe` only if `condition` is true. */
static justIf<T>(condition: boolean, value: T): Maybe<T> {
return condition ? Maybe.just(value) : Maybe.nothing<T>();
}
/** Try to cast a value of type `T` to type `S`. */
static cast<T, S>(type: { new(...agrs: any[]): S; }, value: T): Maybe<S> {
return value instanceof type ? Maybe.just(value) : Maybe.nothing<S>();
}
/** Join two layers of `Maybe` into one. */
static join<T>(maybe: Maybe<Maybe<T>>): Maybe<T> {
return maybe.then(x => x);
}
/** Keep only defined values in array. */
static filterJust<T>(array: Maybe<T>[]): T[] {
return array.filter(x => x.hasValue()).map(x => x.get());
}
/** Try parse integer. */
static parseInt(str: string, radix?: number): Maybe<number> {
const i = parseInt(str, radix);
return Maybe.justIf(!isNaN(i), i);
}
/** Try parse float. */
static parseFloat(str: string): Maybe<number> {
const i = parseFloat(str);
return Maybe.justIf(!isNaN(i), i);
}
/** Try get an element of the `array` at `index`. */
static elemAt<T>(array: T[], index: number): Maybe<T> {
return new Maybe(array[index]);
}
/** Try get a property of the `object` with given `key`. */
static prop<T>(object: { [k: string]: T }, key: string): Maybe<T> {
return new Maybe(object[key]);
}
/** Try to find element in array. Wrapper around Array.find. */
static find<T>(array: T[], predicate: (value: T, index: number, obj: T[]) => boolean): Maybe<T> {
return new Maybe(array.find(predicate));
}
/** Optional value. */
readonly value: T | undefined;
constructor(value: T | undefined | null) {
this.value = value === null
? undefined
: value;
}
/** Check if value is present. */
hasValue() {
return this.value !== undefined;
}
/** Apply a function to the `value` if present. */
map<R>(fn: (value: T) => R): Maybe<R> {
return this.value !== undefined
? Maybe.just(fn(this.value))
: Maybe.nothing<R>();
}
/** Apply a function inside other `Maybe` to the `value`. */
apply<R>(maybeFn: Maybe<(value: T) => R>): Maybe<R> {
return maybeFn.value !== undefined
? this.map(maybeFn.value)
: Maybe.nothing<R>();
}
/** Apply a function returning `Maybe` to the `value`. */
then<R>(fn: (value: T) => Maybe<R>): Maybe<R> {
return this.value !== undefined
? fn(this.value)
: Maybe.nothing<R>();
}
/** Returns the `value` if present or throws an exception. */
get(): T {
if (this.value === undefined) { throw 'Maybe doesn\'t have a value.'; }
return this.value;
}
/** Returns the `value` if present or specified default value. */
getDef(defaultValue: T): T {
return this.mapDef(defaultValue, v => v);
}
/**
* Apply a function to the `value` if present or return
* specified default value.
*/
mapDef<R>(defaultValue: R, fn: (value: T) => R): R {
return this.value !== undefined
? fn(this.value)
: defaultValue;
}
/** Convert to list of zero or one element. */
toList(): T[] {
return this.mapDef([], v => [v]);
}
/** Perform first action if `Maybe` has a value or second action otherwise. */
ifElse(hasValueFn: (value: T) => void, noValueFn: () => void): void {
return this.value !== undefined
? hasValueFn(this.value)
: noValueFn();
}
}
/********************************************************************
* EXAMPLES
********************************************************************/
// Get deeply nested optional field.
// Without `Maybe`:
interface Foo {
barId?: number;
}
interface Bar {
bazId?: number;
}
interface Baz {
value?: string;
}
function getFooById(fooId: number): Foo | undefined { /* get Foo somehow */ return undefined }
function getBarById(barId: number): Bar | undefined { /* get Bar somehow */ return undefined }
function getBazById(bazId: number): Baz | undefined { /* get Baz somehow */ return undefined }
function getValueByFooId(fooId: number): string | undefined {
const foo = getFooById(fooId);
if (foo && foo.barId) {
const bar = getBarById(foo.barId);
if (bar && bar.bazId) {
const baz = getBazById(bar.bazId);
if (baz) {
return baz.value;
}
}
}
return undefined;
}
// With `Maybe`:
interface FooM {
barId: Maybe<number>;
}
interface BarM {
bazId: Maybe<number>;
}
interface BazM {
value: Maybe<string>;
}
function getFooByIdM(fooId: number): Maybe<FooM> { /* get Foo somehow */ return Maybe.nothing() }
function getBarByIdM(barId: number): Maybe<BarM> { /* get Bar somehow */ return Maybe.nothing() }
function getBazByIdM(bazId: number): Maybe<BazM> { /* get Baz somehow */ return Maybe.nothing() }
function getValueByFooIdM(fooId: number): Maybe<string> {
return getFooByIdM(fooId)
.then(foo => foo.barId)
.then(getBarByIdM)
.then(bar => bar.bazId)
.then(getBazByIdM)
.then(baz => baz.value);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment