Skip to content

Instantly share code, notes, and snippets.

@bakerface
Last active April 17, 2025 13:31
Show Gist options
  • Select an option

  • Save bakerface/eb582cb9e0b2045a2211a94b834c2be4 to your computer and use it in GitHub Desktop.

Select an option

Save bakerface/eb582cb9e0b2045a2211a94b834c2be4 to your computer and use it in GitHub Desktop.
Compile-time checks for fluent types in TypeScript.
export class Person {
private _designation: string;
private _first: string;
private _last: string;
static create(): FluentPerson {
return new Person();
}
private constructor() {
this._designation = "";
this._first = "";
this._last = "";
}
withDesignation(first: string) {
this._first = first;
return this;
}
withFirst(first: string) {
this._first = first;
return this;
}
withLast(last: string) {
this._last = last;
return this;
}
toString() {
if (!this._first) {
throw new Error("A first name is required.");
}
if (!this._last) {
throw new Error("A last name is required.");
}
if (this._designation) {
return `${this._designation} ${this._first} ${this._last}`;
}
return `${this._first} ${this._last}`;
}
print() {
console.log(this.toString());
}
}
// use the simple syntax when all functions are required.
// export type FluentPerson = Fluent<Person>;
// use the verbose syntax when some functions are optional.
export type FluentPerson = Fluent<
Person,
"withFirst" | "withLast", // these keys are required
"withDesignation" // these keys are optional
>;
const person = Person.create()
.withDesignation("Mr.") // this call is optional
.withFirst("John") // this call is required
.withLast("Doe"); // this call is required
// this will be a compile-time error if one of the
// required functions was not called.
person.print();
export type Fluent<
T,
Required extends keyof T = FluentKeys<T>,
Optional extends keyof T = never,
> = {
[K in Required | Optional]: ChangeReturn<
T[K],
Exclude<Required, K> extends never
? T
: Fluent<T, Exclude<Required, K>, Exclude<Optional, K>>
>;
};
export type FluentKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => T ? K : never;
}[keyof T];
type ChangeReturn<F, Return> = F extends (...args: infer Args) => any
? (...args: Args) => Return
: never;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment