以类型安全的方式在管道中使用 Ramda 的克隆

Use Ramda's clone in pipe in a type-safe way

我想使用 Ramda 以类型安全的方式克隆和更新对象(受 this idiom 启发),但我无法使其以类型安全的方式工作。

以类型安全的方式更新嵌套对象非常好:

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`

我也可以使用 pipecompose 成功组合函数:

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

然而,一旦我使用 clone 它就失败了:

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[]

这可能是类型问题?或者我在这里遗漏了什么?

(免责声明:我是 Ramda 的核心团队之一。)


Ramda 团队在 TypeScript 方面的专业知识不多。我添加了 definitelytyped 标签,因为该项目保留了通常的 Ramda 类型。

不太了解 TypeScript 类型,我不明白为什么这不起作用,就像我阅读 clone definition:

export function clone<T>(value: T): T;
export function clone<T>(value: readonly T[]): T[];

和相关的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;

我觉得一切都很好。我想知道您是否需要将 addManagerincreaseBudget 的结果声明为 Person。但这只是一个 non-TS 人的猜测。


我回答主要是因为我想指出,对于许多用途,您不需要使用 clone,因为 assocPath 已经对它正在改变的任何数据做了等效的处理。

const person2 = increaseBudget  (10000) (person1)
person2 == person1 //=> false
person2 .department == person1 .department //=> false

当然 assocPath 在可以和不进行完整克隆的地方使用结构共享:

const person2 = assocPath ('startDate', '2014-07-12') (person1)
person2 .department == person1 .department //=> true

但对于许多用途,特别是如果使用 Ramda 或其他不可变技术进行进一步修改,clone 根本就没有必要。

-- 斯科特

当使用R.pipe(或R.compose)与其他Ramda泛型函数(例如R.clone)时,TS有时无法推断出正确的类型,而创建的实际签名功能。

注意:我正在使用 Ramda - 0.28.0 和@types/ramda - 0.28.8.

在你的例子中,我们希望 Ramda 使用这个签名——一个参数列表传递给创建的函数 (TArgs),然后是 3 return 类型的管道函数 (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;

由于 Ramda 不推断它们,我们需要显式添加它们 (sandbox):

const addManagerAndUpdateBudget = pipe<[Person], Person, Person, Person>(
  clone,
  addManager('MichaelScott'),
  increaseBudget(10000)
);

参数 - 具有单个 Person 的元组,每个 return 值也是一个 Person。我们需要声明所有这些,以便 TS 将使用我们需要的特定签名。

另一个选项是在管道中显式键入第一个函数,因此 TS 可以使用它来推断其他类型 (sandbox):

const addManagerAndUpdateBudget = pipe(
  clone as (person: Person) => Person,
  addManager('MichaelScott'),
  increaseBudget(10000)
);