I'd like to use Ramda to clone and update objects in a type-safe way (inspired by this idiom) but I can't get it working in a type-safe way.
Updating a nested object works in a type-safe way perfectly fine:
interface Person {
name: string
department: {
name: string
budget: number
manager?: string
}
}
const personX: Person = {
name: 'Foo Bar',
department: {
name: 'x',
budget: 2000,
},
}
const addManager = (name: string): (input: Person) => Person => assocPath([
'department',
'manager',
], name)
const x = addManager('Michael Scott')(personX) // x is of type `Person`
I can also successfully combine functions using pipe
or compose
:
const addManager = (name: string): (input: Person) => Person => assocPath([
'department',
'manager',
], name)
const increaseBudget = (budget: number): (input: Person) => Person => assocPath([
'department',
'budget',
], budget)
const addManagerAndUpdateBudget = pipe(addManager('MichaelScott'), increaseBudget(10000))
const x = addManagerAndUpdateBudget(personX) // x is still of type Person
However, as soon as I use clone
it fails:
const addManager = (name: string): (input: Person) => Person => assocPath([
'department',
'manager',
], name)
const increaseBudget = (budget: number): (input: Person) => Person => assocPath([
'department',
'budget',
], budget)
const addManagerAndUpdateBudget = pipe(clone, addManager('MichaelScott'), increaseBudget(10000))
const x = addManagerAndUpdateBudget(personX) // Person is not assignable to readonly unknown[]
Might this be an issue with the types? Or am I missing something here?
When using R.pipe
(or R.compose
) with other Ramda generic functions (such as R.clone
) TS sometimes fails to infer the correct types, and the actual signature of the created function.
Note: I'm using Ramda - 0.28.0 and @types/ramda - 0.28.8.
In your case we want Ramda to use this signature - A list of arguments pass to the created function (TArgs
), and then 3 return types of the piped functions (R1
, R2
, R3
):
export function pipe<TArgs extends any[], R1, R2, R3>(
f1: (...args: TArgs) => R1,
f2: (a: R1) => R2,
f3: (a: R2) => R3,
): (...args: TArgs) => R3;
Since Ramda doesn't infer them, we'll need to add them explicitly (sandbox):
const addManagerAndUpdateBudget = pipe<[Person], Person, Person, Person>(
clone,
addManager('MichaelScott'),
increaseBudget(10000)
);
Arguments - a tuple with a single Person
, and each return value is also a Person
. We need to state all of them, so that TS would use the specific signature we need.
Another option is explicitly type the 1st function in the pipe, so TS can use it to infer the other types (sandbox):
const addManagerAndUpdateBudget = pipe(
clone as (person: Person) => Person,
addManager('MichaelScott'),
increaseBudget(10000)
);