Home > Software engineering >  Use Ramda's clone in pipe in a type-safe way
Use Ramda's clone in pipe in a type-safe way

Time:04-11

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

  • Related