Last active
November 6, 2022 06:50
-
-
Save vagarenko/6ad5a81c2b2e7523b26be3737280b2f8 to your computer and use it in GitHub Desktop.
Maybe type in Typescript.
This file contains 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
/** | |
* 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