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?
CodePudding user response:
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)
);
CodePudding user response:
(Disclaimer: I'm one of Ramda's core team.)
The Ramda team does not have a great deal of expertise in TypeScript. I've added the definitelytyped
tag, as that project maintains the usual Ramda typings.
Not knowing TypeScript typings well, I don't understand why this doesn't work, as when I read the clone
definition:
export function clone<T>(value: T): T;
export function clone<T>(value: readonly T[]): T[];
and the relevant pipe
definition
export function pipe<TArgs extends any[], R1, R2, R3>(
f1: (...args: TArgs) => R1,
f2: (a: R1) => R2,
f3: (a: R2) => R3,
): (...args: TArgs) => R3;
everything looks right to me. I wonder if you need to declare the resulting of addManager
and increaseBudget
as Person
. But that's just a guess, from a non-TS person.
I answered chiefly because I want to point out that for many uses, you will not need to use clone
because assocPath
already does the equivalent for any data it's altering.
const person2 = increaseBudget (10000) (person1)
person2 == person1 //=> false
person2 .department == person1 .department //=> false
Of course assocPath
uses structural sharing where it can and does not do a full clone:
const person2 = assocPath ('startDate', '2014-07-12') (person1)
person2 .department == person1 .department //=> true
But for many uses, especially if further modifications are done using Ramda or with other immutable techniques, clone
is simply unnecessary.
-- Scott