Sometimes you have to pass just an obscene amount of data to a constructor or you have a big, fat object that you have to send somewhere.
function builder<
T extends Record<string, (arg: any) => NonNullable<unknown> | null>,
>(requirements: T) {
type InputShape = {
[K in keyof T]: Parameters<T[K]>[0];
};
type RequiredKeys = {
[K in keyof InputShape]: IsInUnion<undefined, InputShape[K]> extends true
? never
: K;
}[keyof InputShape];
type OptionalKeys = {
[K in keyof InputShape]: IsInUnion<undefined, InputShape[K]> extends true
? K
: never;
}[keyof InputShape];
type OutputShape = {
[K in keyof T]: ReturnType<T[K]>;
};
return new (class LocalBuilder<
THave extends Partial<OutputShape> = Partial<OutputShape>,
> {
private obj: THave;
constructor(obj: THave) {
this.obj = obj;
return this;
}
public withOnly<
TKey extends Exclude<keyof InputShape, keyof THave>,
TValue extends InputShape[TKey],
>(key: TKey, value: TValue) {
return this.with(key, value);
}
public with<TKey extends keyof InputShape, TValue extends InputShape[TKey]>(
key: TKey,
value: TValue,
) {
const fn = requirements[key];
if (fn === undefined) {
throw new ReferenceError(`No key ${String(key)} in object`);
}
(this.obj as unknown as InputShape)[key] = fn(value);
type NewHave = Omit<THave, TKey> & Record<TKey, OutputShape[TKey]>;
return this as unknown as LocalBuilder<
NewHave extends Partial<OutputShape> ? NewHave : never
>;
}
public build(
this: THave extends Pick<OutputShape, RequiredKeys> ? this : never,
) {
type UnsetKeys = Exclude<OptionalKeys, keyof THave>;
type FinalObject = THave & {
[K in UnsetKeys]: undefined;
};
return this.obj as Simplify<FinalObject>;
}
})<{}>({});
}
function optional<TArg, TRet, TFallback extends (() => TRet) | undefined>(
fn: (arg: TArg) => TRet,
fallback?: TFallback,
) {
const newFn = (arg: TArg | undefined) => {
if (arg === undefined) {
if (fallback) {
return fallback();
} else {
return null;
}
}
return fn(arg);
};
type NewReturn = TFallback extends undefined ? TRet | null : TRet;
return newFn as (arg: TArg | undefined) => NewReturn;
}
const bldr = builder({
a: (x: string) => x,
b: (x: string) => x,
c: (x: string) => ({ x }),
d: optional((x: number) => x),
e: optional(
(x: number) => BigInt(x * 2),
() => 0n,
),
});
const k = bldr
.with("b", "b val")
.with("a", "a val")
.with("c", "c val")
.with("d", 4)
.with("e", 3)
.build();
assert.deepStrictEqual(k, {
a: "a val",
b: "b val",
c: {
x: "c val",
},
d: 4,
e: 6n,
});