Skip to content

Instantly share code, notes, and snippets.

@ChristianIvicevic
Last active February 26, 2021 23:56
Show Gist options
  • Save ChristianIvicevic/6f4b919b2edfe88c0eca515e5a4ad11b to your computer and use it in GitHub Desktop.
Save ChristianIvicevic/6f4b919b2edfe88c0eca515e5a4ad11b to your computer and use it in GitHub Desktop.
type Class<T = unknown, Arguments extends unknown[] = unknown[]> = new (
...args: Arguments
) => T;
const nameof = <T>(name: keyof T) => name;
type Builder<T> = {
[K in keyof Omit<
T,
{
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
}[keyof T]
>]-?: (value: T[K]) => Builder<T>;
} & {
build(): T;
};
///////////////////////////////////////////////////////////////////////////////
// FUNCTIONAL BUILDER
///////////////////////////////////////////////////////////////////////////////
export const createBuilder = <T>(Constructor?: Class<T>) => {
const draft: Record<string, unknown> = {};
const builder = new Proxy(
{} as Builder<T>,
{
get(_target, property) {
if (property === nameof<Builder<unknown>>('build')) {
if (Constructor === undefined) {
return () => ({ ...draft });
}
return () => Object.assign(new Constructor(), { ...draft });
}
return (value: unknown): unknown => {
draft[property as string] = value;
return builder;
};
},
},
);
return builder;
};
///////////////////////////////////////////////////////////////////////////////
// SAMPLE TYPE AND CLASS
///////////////////////////////////////////////////////////////////////////////
type SampleType = {
value1: number;
value2: string;
value3: boolean;
};
class SampleClass {
public publicNumber?: number;
protected protectedString?: string;
private privateBoolean?: boolean;
public stillWorks() {
console.log(
`${this.publicNumber} ${this.protectedString} ${this.privateBoolean}`,
);
}
}
///////////////////////////////////////////////////////////////////////////////
// FUNCTIONAL BUILDER SAMPLE
///////////////////////////////////////////////////////////////////////////////
const objectFromType = createBuilder<SampleType>()
.value1(1)
.value2('2')
.value3(false)
.build();
console.log(JSON.stringify(objectFromType));
const objectFromClass = createBuilder(SampleClass).publicNumber(1).build();
objectFromClass.stillWorks();
const objectFromClass2 = createBuilder(SampleClass)
.publicNumber(1)
// Property 'protectedString' does not exist on type 'Builder<SampleClass>'.
.protectedString('2')
// Property 'privateBoolean' does not exist on type 'Builder<SampleClass>'.
.privateBoolean(false)
.build();
///////////////////////////////////////////////////////////////////////////////
// BUILDER MIXIN
///////////////////////////////////////////////////////////////////////////////
export const WithBuilder = <
T extends new (...args: any[]) => any,
R = T extends new (...args: unknown[]) => infer R ? R : never
>(
Constructor: T,
) =>
class extends Constructor {
public static builder() {
return createBuilder<R>(Constructor);
}
};
const ClassWithBuilderMixin = WithBuilder(
class {
public value1?: number;
public value2?: string;
public value3?: boolean;
},
);
const newObject = ClassWithBuilderMixin.builder()
.value1(1)
.value2('2')
.value3(false)
.build();
console.log(JSON.stringify(newObject));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment