I am building a simulator where an object is transformed step by step through various functions using pipe from fp-ts. To make the code more expressive, I’m using higher-order functions to create these transformers.
Here’s an example of what I want my code to look like:
pipe(
{ name: "Nick" },
merge({ age: 34 }),
merge({ job: "programmer" }),
merge({ married: true })
);
Ideally, I want this to be type-safe by specifying types for pipe, as shown below:
type Guy = { name: string };
type GuyWithAge = Guy & { age: number };
type GuyWithAgeAndJob = GuyWithAge & { job: string };
type GuyWithAgeAndJobAndMarried = GuyWithAgeAndJob & { married: boolean };
pipe<Guy, GuyWithAge, GuyWithAgeAndJob, GuyWithAgeAndJobAndMarried>(
{ name: "Nick" },
merge({ age: 34 }),
merge({ job: "programmer" }),
merge({ married: true })
);
I want TypeScript to infer the types at each step, such that it only accepts the difference between the types, ensuring that the object transformation is type-safe.
I managed to implement this with generics and TypeScript utilities, but there’s a caveat:
// Merge is like A & B but it overwrites properties on A with B. Like {...A, ...B}
type Merge<First extends object, Second extends object> = Omit<
First,
keyof Second
> &
Second;
// A delta is the object that you merge with From to create To
export type Delta<From extends object, To extends object> = {
[K in keyof To as K extends keyof From
? To[K] extends From[K]
? never
: K
: K]: To[K];
};
const merge =
<First extends object, Second extends object>(
delta: Delta<First, Second>,
) =>
(obj: First): Merge<First, Delta<First, Second>> => {
const merged = {
...obj,
...delta,
};
return merged;
};
type Guy = { name: string };
type GuyWithAge = Guy & { age: number };
type GuyWithAgeAndJob = GuyWithAge & { job: string };
type GuyWithAgeAndJobAndMarried = GuyWithAgeAndJob & { married: boolean };
const guy = pipe<Guy, GuyWithAge, GuyWithAgeAndJob, GuyWithAgeAndJobAndMarried>(
{ name: "Nick" },
merge<Guy, GuyWithAge>({ age: 34 }),
merge<GuyWithAge, GuyWithAgeAndJob>({ job: "programmer" }),
merge<GuyWithAgeAndJob, GuyWithAgeAndJobAndMarried>({ married: true })
);
The issue is that I need to explicitly specify the types each time I call merge, which leads to verbosity.
My question is: Is there any way to get TypeScript to automatically infer the correct generics for each merge call within the pipe, so that I don’t have to manually specify the types at each step?