A quest to use the JavaScript pipeline operator's mental model using inspiration from Kotlin's let
and also
scope functions.
This library was original built to assist in writing conditional Kysely queries.
- Chaining
- Really Great Type Inference
- Async-Await Support
- Mixing
.let()
and.also()
# pnpm
pnpm add --save scope-utilities
# npm
npm install --save scope-utilities
import { scope, run, returnOf } from "scope-utilities";
The .let()
function is useful for transforming the value. The let function takes in a function, FUNC
that accepts the current scoped VALUE
as input.
The return of the .let()
function is the return of FUNC
but wrapped in a scope()
call itself. This allows chaining.
See the examples below for more details.
const result = scope(1)
.let((x) => x + 1)
.let((x) => x * 2)
.let((x) => x - 1)
.value();
console.log(result); // 3
Using a single async function in a chain will cause the entire .value()
expression to be awaitable (promise).
Note: the subsequent .let()
or .also()
need not be async.
async function double(x: number) {
return await Promise.resolve(x * 2);
}
const result = await scope(1)
.let((x) => x + 1)
.let(async (x) => await double(x))
.let((x) => x - 1)
.value();
console.log(result); // 3
const kyselyQuery = scope(kysely.selectFrom("media"))
.let((query) =>
input.shop_id ? query.where("shop_id", "=", input.shop_id) : query
)
.let((query) =>
query
.where("media.type", "=", input.type)
.where("media.deleted_at", "is", null)
)
.value();
await kyselyQuery.execute();
The .also()
function is ideal for logging or debugging, as well as containing mutations to a mutable object to a single expression.
Think of it like a function that initializes a variable, makes modifications to the object contained within the variable and returns the original variable.
Similar to .let
, .also
takes in a function, FUNC
that accepts the current scoped VALUE
as input.
The return value of the .also()
function is simply the current scoped value (NOT the return value of FUNC
) but wrapped in a scope()
call itself. This allows chaining.
See the examples below for more details.
Note: the order of operations is always maintained
irrespective of if you mix .let()
or .order()
const sameTimeNextWeekEpoch = scope(new Date())
.also((date) => {
date.setDate(date.getDate() + 7);
})
.let((date) => date.getTime() / 1000)
.value();
async function randomInt(x: number) {
return await Promise.resolve(Math.round(Math.random() * 5));
}
const sameTimeAfterFewDays = await scope(new Date())
.also(async (date) => {
date.setDate(date.getDate() + (await randomInt()));
})
.let((date) => date.getTime() / 1000)
.value();
The .also()
function is useful for logging or debugging.
const result = await scope(1)
.let((x) => x + 1)
.let(async (x) => await double(x))
.also((x) => {
console.log(x);
})
.let((x) => x - 1)
.value();
This is a simple function that just wraps the return value of a function in a scope()
call.
const result = run(() => {
return 1 + 1;
}).value();
console.log(result); // 2
This is an even simpler function that just returns the return value of a function call.
I created this because I think IIFEs are ugly.
const result = returnOf(() => {
return 1 + 1;
});
console.log(result); // 2
export interface IScoped<T> {
let<RT>(
func: (value: OptionallyUnwrapPromise<T>) => RT
): IScoped<
T extends Promise<infer REAL_T> ? Promise<OptionallyUnwrapPromise<RT>> : RT
>;
also<RT extends void | Promise<void>>(
func: (value: OptionallyUnwrapPromise<T>) => RT
): IScoped<
T extends Promise<infer REAL_T>
? Promise<OptionallyUnwrapPromise<T>>
: RT extends Promise<any>
? Promise<OptionallyUnwrapPromise<T>>
: T
>;
value(): T;
}
export function scope<T>(value: T): IScoped<T>;
export function run<T>(func: () => T): IScoped<T>;
export function returnOf<T>(func: () => T): T;
MIT