Skip to content

Instantly share code, notes, and snippets.

@TotallyNotChase
Last active March 27, 2022 13:37
Show Gist options
  • Save TotallyNotChase/ce14e04c0e29902de27b9fbb41d3e429 to your computer and use it in GitHub Desktop.
Save TotallyNotChase/ce14e04c0e29902de27b9fbb41d3e429 to your computer and use it in GitHub Desktop.
Scoped, yet too unscoped - extensible records API in typescript, through expressive abstraction
import { stretch, stretchP } from './implementation';
interface IApplication {
wow: boolean;
great: string;
}
function stretchExample0(cond: boolean): IApplication {
// Reach for an `IApplication`, but start with `{}`.
const app = stretch<IApplication>()
.extend('wow', cond)
.extendWith('great', _ => {
if (cond) {
return 'yay';
} else {
return 'nay';
}
})
.pure();
return app;
}
function stretchExample1(cond: boolean): IApplication {
// Reach for an `IApplication`, but start with `{}`.
const app = stretch<IApplication>()
.extend('wow', cond)
// @ts-expect-error
.extendWith('great', _ => {
if (cond) {
return 1;
} else {
return 2;
}
// ^ Invalid return type for 'great' assignment ('great' expects string).
// And finally, the error is here: "Type 'number' is not assignable to type 'string'."
})
.pure();
return app;
// ^ No errors here, take care of the error above.
}
function stretchExample2(cond: boolean): IApplication {
// Reach for an `IApplication`, but start with `{}`.
const app0 = stretch<IApplication>().extend('wow', cond);
// You can also separate out the methods, but this is silly.
const app1 = app0.extendWith('great', _ => {
if (cond) {
return 'yay';
} else {
return 'nay';
}
});
return app1.pure();
}
function stretchExample3(cond: boolean): IApplication {
// You can also extend with several keys at once.
const app = stretch<IApplication>()
.extendMany((kv, _) => {
const init = kv('wow', cond);
if (cond) {
return [init, kv('great', 'yay')];
} else {
return [init, kv('great', 'nay')];
}
})
.pure();
return app;
}
async function stretchPExample0(cond: boolean): Promise<IApplication> {
// Reach for an `IApplication`, but start with `{}`.
const app = await stretchP<IApplication>()
.extend('wow', cond)
.extendWith('great', async _ => {
if (cond) {
return 'yay';
} else {
return 'nay';
}
})
.pure();
return app;
}
async function stretchPExample1(cond: boolean): Promise<IApplication> {
// Reach for an `IApplication`, but start with `{}`.
const app = stretchP<IApplication>()
.extend('wow', cond)
// @ts-expect-error
.extendWith('great', async _ => {
if (cond) {
return 1;
} else {
return 2;
}
// ^ Invalid return type for 'great' assignment ('great' expects string).
// And finally, the error is here: "Type 'number' is not assignable to type 'string'."
});
return app.pure();
// ^ No errors here, take care of the error above.
}
async function stretchPExample2(cond: boolean): Promise<IApplication> {
// Reach for an `IApplication`, but start with `{}`.
const app0 = stretchP<IApplication>().extend('wow', cond);
// You can also separate out the methods, but this is silly.
const app1 = app0.extendWith('great', async _ => {
if (cond) {
return 'yay';
} else {
return 'nay';
}
});
return app1.pure();
}
async function stretchPExample3(cond: boolean): Promise<IApplication> {
// You can also extend with several keys at once.
const app = stretchP<IApplication>().extendMany(async (kv, _) => {
const init = kv('wow', cond);
if (cond) {
return [init, kv('great', 'yay')];
} else {
return [init, kv('great', 'nay')];
}
});
return app.pure();
}
class TypedKV<Target, K extends keyof Target> {
constructor(private readonly k: K, private readonly v: Target[K]) {}
pure(): [K, Target[K]] {
return [this.k, this.v];
}
}
// Extend to a target. Reach for the skies.
export class Stretch<Target, T = {}> {
private typedKV: <K extends keyof Target>(k: K, v: Target[K]) => TypedKV<Target, K>;
constructor(private readonly x: T) {
this.typedKV = (k, v) => new TypedKV(k, v);
}
// Extend with a key and a value, where the value to be assigned must follow from `Target[K]`
// where `K` is indeed a key of `Target`. The user may not choose the type of the value freely.
extend<K extends keyof Target>(k: K, v: Target[K]): Stretch<Target, T & { [k in K]: Target[K] }> {
const extension = { [k]: v } as { [k in K]: Target[K] };
const res = { ...this.x, ...extension };
return new Stretch(res);
}
// extend as above, but this time with a function to handle stateful computation perhaps.
extendWith<K extends keyof Target>(k: K, vc: (x: T) => Target[K]): Stretch<Target, T & { [k in K]: Target[K] }> {
return this.extend(k, vc(this.x));
}
// Extend with several keys, in the form of key value pairs.
extendMany<K extends keyof Target, L extends readonly TypedKV<Target, keyof Target>[]>(
vc: (typedKV: typeof this.typedKV, x: T) => [TypedKV<Target, K>, ...L]
): Stretch<Target, AssignMany<T & { [k in K]: Target[K] }, L>> {
const [kv0, ...adds] = vc(this.typedKV, this.x);
const [k, v] = kv0.pure();
const extension = { [k]: v } as { [k in K]: Target[K] };
const initial: T & typeof extension = { ...this.x, ...extension };
const additions = Object.fromEntries(adds.map(x => x.pure())) as AssignMany<{}, typeof adds>;
const combied = { ...initial, ...additions } as AssignMany<typeof initial, typeof adds>;
return new Stretch(combied);
}
// Extract essence. Unwrap the Stretch layer.
// N.B: You can call `pure` whenever you want - not necessarily once you have reached `Target` construction fully.
// N.B: It's possible to make `pure` callable only once `T = Target`, but why?
pure(): T {
return this.x;
}
}
// Good ol' `Stretch`, promisified.
export class StretchP<Target, T = {}> {
private typedKV: <K extends keyof Target>(k: K, v: Target[K]) => TypedKV<Target, K>;
constructor(private readonly xprom: Promise<T>) {
this.typedKV = (k, v) => new TypedKV(k, v);
}
// Extend with a key and a value, where the value to be assigned must follow from `Target[K]`
// where `K` is indeed a key of `Target`. The user may not choose the type of the value freely.
extend<K extends keyof Target>(k: K, v: Target[K]): StretchP<Target, T & { [k in K]: Target[K] }> {
const extension = { [k]: v } as { [k in K]: Target[K] };
const res = this.xprom.then(x => ({ ...x, ...extension }));
return new StretchP(res);
}
// extend as above, but this time with a function to handle stateful computation perhaps.
extendWith<K extends keyof Target>(
k: K,
vc: (x: T) => Promise<Target[K]>
): StretchP<Target, T & { [k in K]: Target[K] }> {
const res = this.xprom.then(x =>
vc(x).then(v => {
const newStretch = this.extend(k, v);
return newStretch.pure();
})
);
return new StretchP(res);
}
// Extend with several keys, in the form of key value pairs.
extendMany<K extends keyof Target, L extends readonly TypedKV<Target, keyof Target>[]>(
vc: (typedKV: typeof this.typedKV, x: T) => Promise<[TypedKV<Target, K>, ...L]>
): StretchP<Target, AssignMany<T & { [k in K]: Target[K] }, L>> {
const res: Promise<AssignMany<T & { [k in K]: Target[K] }, L>> = this.xprom.then(x =>
vc(this.typedKV, x).then(([kv0, ...adds]) => {
const [k, v] = kv0.pure();
const extension = { [k]: v } as { [k in K]: Target[K] };
const initial: T & typeof extension = { ...x, ...extension };
const additions = Object.fromEntries(adds.map(x => x.pure())) as AssignMany<{}, typeof adds>;
return { ...initial, ...additions } as AssignMany<typeof initial, typeof adds>;
})
);
return new StretchP(res);
}
// Extract essence. Unwrap the Stretch layer.
// N.B: You can call `pure` whenever you want - not necessarily once you have reached `Target` construction fully.
// N.B: It's possible to make `pure` callable only once `T = Target`, but why?
pure(): Promise<T> {
return this.xprom;
}
}
type Head<L> = L extends [infer H, ...any[]] ? H : never;
type Tail<L> = L extends [any, ...infer T] ? T : never;
type AssignMany<C, L> = L extends []
? C
: Head<L> extends TypedKV<infer Target, infer K>
? K extends keyof Target
? AssignMany<C & { [k in K]: Target[K] }, Tail<L>>
: never
: never;
export function stretch<Target>(): Stretch<Target, {}> {
return new Stretch({});
}
/** Like 'stretch' but start with some concrete value.
Example: `stretchWith<IApplication, Pick<IApplication, 'wow'>>({ wow: cond })`
*/
export function stretchWith<Target, T>(x: T): Stretch<Target, T> {
return new Stretch(x);
}
export function stretchP<Target>(): StretchP<Target, {}> {
return new StretchP(Promise.resolve({}));
}
/** Like 'stretchP' but start with some concrete value.
Example: `stretchWithP<IApplication, Pick<IApplication, 'wow'>>({ wow: cond })`
*/
export function stretchWithP<Target, T>(x: T): StretchP<Target, T> {
return new StretchP(Promise.resolve(x));
}