Friendly monadic types inspired by Rust. Fully typed Do-notation and misc functional programming utils.
Warning
This project is still a work in progress, breaking changes is likely. Semantic versioning does not necessarily apply until a 1.0 release
Note
Instances of Option, Result, Task and Iter are frozen (immutable) and are null-prototype objects
There is a lot of prior art similar to this package, such as Effect-TS. DoTS aims to be pragmatic and provide a minimal api surface to get the job done with the lowest possible barrier of entry for developers new to the functional programming paradigm. However, you should probably use Effect-TS. It's an awesome library!
Do notation in typescript is not natively supported, but can be achieved using generator functions. However, the
typescript types for Generator and Iterator assumes the same type T is yielded from a generator, to work around this, we
can yield a yielder instead, which is a generator function that yields itself. This is why the monadic types here
implements [Symbol.iterator] and also why the yield*
operator is required in a do-block.
import { Do, Done, Fail, None, type Option, Some, Task, taskify } from 'dots'
export class RequestError extends Error {
constructor(
public readonly status: number,
public readonly content: {
message: string
status: string
body: Option<any>
},
public readonly inner: Option<Error>,
) {
super(content.message)
}
}
const _taskFetch = taskify(fetch)
const taskFetch: typeof _taskFetch = (...args) =>
_taskFetch(...args).mapFailure((err) => {
// Fetch rejected with no response from the uri
return new RequestError(
500,
{
message: (err as any)?.message ?? 'Unknown Error',
status: 'Unknown',
body: None(),
},
Some(err as any),
)
})
export const request = Do.bind(function* <T>(
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
uri: string,
headers: Record<PropertyKey, any> = {},
body: Record<PropertyKey, any> = {},
) {
const response = yield* taskFetch(uri, {
method,
headers,
body: method !== 'GET' ? JSON.stringify(body) : undefined,
})
const json = yield* Task(response.json()).mapFailure((err) => {
return new RequestError(
500,
{
message: (err as any)?.message ?? 'Invalid JSON Response',
status: 'Unknown',
body: None(), // could be response.text() instead
},
Some(err as any),
)
})
if (response.ok) {
return Done<T, RequestError>(json as NonNullable<T>)
}
return Fail<T, RequestError>(
new RequestError(
response.status,
{
status: response.statusText,
message: 'Response indicated not OK',
body: Some(json),
},
None(),
),
)
})