redux 从函数组合中的链推断类型参数[compose]

swvgeqrz  于 2023-02-04  发布在  其他
关注(0)|答案(1)|浏览(125)

最初这个问题是两个问题“拼凑”在一起,但经过评论中的讨论和@jcalz的一些急需的帮助,我们已经设法布里格它更接近现在的样子。

  • 您可以在末尾找到完整的代码示例,以方便复制粘贴 *

问题:

  • 您可以在下面找到类型定义和代码示例 *

我试图找出一种方法,如何“组合”(如在函数组合中)多个函数,假设这些函数通过使用附加属性“扩展”单个对象来修改它,将其组合成一个执行所有扩展并正确键入的单个函数。
所讨论的函数是StoreEnhancers<Ext>(其中Ext表示扩展结果对象的普通对象),并且它们的合成结果也应该是StoreEnhancer<ExtFinal>,其中ExtFinal应该是传递到合成中的每个增强器的所有Ext的并集。
无论我怎么尝试,使用扩展操作符(...)传递一个数组,我似乎都无法编写一个组合函数,该函数能够用多个StoreEnhancers扩展一个对象,并允许typescript推断最终的Ext,以便我可以访问这些增强器添加的所有属性。

以下是一些定义,以了解更多上下文:

首先,我们可以将StoreEnhancer定义为接受StoreCreatorEnahncedStoreCreator并返回EnhancedStoreCreator的函数。或者用更易于理解的术语来说,它将接受另一个函数作为参数,用于创建我们称之为“存储对象”的函数。然后,存储增强器将“增强”此存储对象,方法是向其添加更多属性。并且将返回商店对象的“增强”版本。
因此,让我们定义类型(为了简单起见,非常基本)

type Store = {
   tag: 'basestore' // used just to make Store distinct from a base {}
}
type StoreCreator = () => Store
type EnhancedStoreCreator<Ext> = () => Store & Ext

// essentially one could say that:
// StoreCreator === EnhancedStoreCreator<{}>
// which allows us to define a StoreEnhancer as such:

type StoreEnhancer<Ext> = <Prev>(createStore: EnhancedStoreCreator<Prev>) => EnhancedStoreCreator<Ext & Prev>

一个实现可能看起来像这样:

const createStore: StoreCreator = () => ({ tag: 'basestore' })

const enhanceWithFeatureA: StoreEnhancer<{ featureA: string }> = createStore => () => {
  const prevStore = createStore()
  return { ...prevStore, featureA: 'some string' }
}

const enhanceWithFeatureB: StoreEnhancer<{ featureB: number }> = createStore => () => {
  const prevStore = createStore()
  return { ...prevStore, featureB: 123 }
}

const createStoreWithA = enhanceWithFeatureA(createStore)
const createStoreWithAandB = enhanceWithFeatureB(createStoreWithA)

const store = storeCreatorWithFeatureAandB()
console.log(store)
//  { 
//    tag: 'baseStore',
//    featureA: 'some string'
//    featureB: 123
//  }

包含新(更新)代码的代码沙箱链接为here
Codesandbox链接与原始问题的代码是here

bt1cpqcv

bt1cpqcv1#

我们的目标是编写一个composeEnhancers()函数,它接受可变数量的参数,每个参数都是某个TIStoreEnhancer<TI>值;也就是说,对于某些类型T0TN,自变量将是元组类型[StoreEnhancer<T0>, StoreEnhancer<T1>, StoreEnhancer<T2>,..., StoreEnhancer<TN>]),并且它应该返回类型StoreEnhancer<R>的值,其中R是所有TI类型的交集;也就是StoreEnhancer<T0 & T1 & T2 & ... & TN>
在我们实现这个函数之前,让我们写出它的调用签名来设计它的类型。从上面的描述来看,我们似乎在处理一个底层的元组类型[T0, T1, T2, ... , TN],它被 * mapped * 成为输入类型。让我们调用元组类型T,并假设在每个类似于数字的索引I处,元素T[I]被Map到StoreEnhancer<T[I]>。幸运的是,这个操作用mapped type来表示非常简单,因为对数组/元组进行操作的Map类型也会产生数组/元组。
所以现在我们有

declare function composeEnhancers<T extends any[]>(
    ...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<???>;

其中rest参数是相关的Map元组类型。请注意,此Map类型是 * 同态 *(参见What does "homomorphic mapped type" mean?),因此编译器可以相当容易地从Map类型的值推断T(这种行为被称为"从Map类型推断",它 * 曾经 * 被记录在这里,但手册的新版本似乎没有提到它)。因此,如果您调用composeEnhancers(x, y, z),其中x的类型为StoreEnhancer<X>y的类型为StoreEnhancer<Y>z的类型为StoreEnhancer<Z>,则编译器将很容易推断T[X, Y, Z]
好的,那么返回类型是什么呢?我们需要用一个类型来替换???,这个类型表示T中所有元素的交集,我们给它一个名字:

declare function composeEnhancers<T extends any[]>(
    ...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<TupleToIntersection<T>>;

现在我们需要定义TupleToIntersection,这里有一个可能的实现:

type TupleToIntersection<T extends any[]> =
    { [I in keyof T]: (x: T[I]) => void }[number] extends
    (x: infer U) => void ? U : never;

这是使用这样的特性:如果推理位置处于 * 逆变 * 位置,则条件类型中的类型推理将生成候选类型的交集(参见Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript)比如函数参数,所以我把TMap到一个每个元素都是函数参数的版本,把它们组合成一个函数的并集,然后推断出这个函数参数的类型,这就变成了一个交集,这和Transform union type to intersection type中所展示的技术很相似。
好了,现在我们有了调用签名。让我们确保调用者看到了所需的行为:

const enhanceWithFeatureA: StoreEnhancer<{ featureA: string }> =
    cs => () => ({ ...cs(), featureA: 'some string' });

const enhanceWithFeatureB: StoreEnhancer<{ featureB: number }> =
    cs => () => ({ ...cs(), featureB: 123 });

const enhanceWithFeatureC: StoreEnhancer<{ featureC: boolean }> =
    cs => () => ({ ...cs(), featureC: false });

const enhanceWithABC = composeEnhancers(
    enhanceWithFeatureA, enhanceWithFeatureB, enhanceWithFeatureC
);
/* const enhanceWithABC: StoreEnhancer<{
    featureA: string;
} & {
    featureB: number;
} & {
    featureC: boolean;
}> */

看起来不错; enhanceWithABC值是单个StoreEnhancer,其类型参数是输入StoreEnhancer s的类型参数的交集。
现在我们基本上已经完成了,函数还需要实现,而且实现已经足够简单了,但不幸的是,因为调用签名相当复杂,所以编译器无法验证实现是否真的完全符合调用签名:

function composeEnhancers<T extends any[]>(
    ...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<TupleToIntersection<T>> {
    return creator => enhancers.reduce((acc, e) => e(acc), creator); // error!
    // Type 'EnhancedStoreCreator<Prev>' is not assignable to type 
    // 'EnhancedStoreCreator<TupleToIntersection<T> & Prev>'.
}

这在运行时是可行的,但是编译器并不知道the array reduce() method会输出正确类型的a值,它知道你会得到一个EnhancedStoreCreator,但并不特别涉及TupleToIntersection<T>,这是TypeScript语言的一个本质上的限制;reduce()的类型不能足够通用,甚至不能表达从底层循环的开始到结束的类型的渐进变化;参见Typing a reduce over a Typescript tuple
所以最好不要尝试,我们应该努力抑制错误,只是小心地让自己相信我们的实现是正确编写的(因为编译器不能为我们这样做)。
一种方法是删除"turn off typechecking" any类型,其中的故障点为:

function composeEnhancers<T extends any[]>(
    ...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<TupleToIntersection<T>> {
    return (creator: EnhancedStoreCreator<any>) =>
            // ------------------------> ^^^^^
        enhancers.reduce((acc, e) => e(acc), creator); // okay
}

现在没有错误了,这已经是我们能做的最好的了,还有其他方法可以抑制错误,比如类型Assert或者单调用签名重载,但是我不会离题,在这里详细讨论这些方法。
现在,我们已经有了类型和实现,让我们确保enhanceWithABC()函数按预期工作:

const allThree = enhanceWithABC(createStore)();
/* const allThree: Store & {
    featureA: string;
} & {
    featureB: number;
} & {
    featureC: boolean;
} & {
    readonly tag: "basestore";
} */

console.log(allThree);
/* {
  "tag": "basestore",
  "featureA": "123",
  "featureB": 123,
  "featureC": false
} */

console.log(allThree.featureB.toFixed(2)) // "123.00"

看起来不错!
Playground代码链接

相关问题