最初这个问题是两个问题“拼凑”在一起,但经过评论中的讨论和@jcalz的一些急需的帮助,我们已经设法布里格它更接近现在的样子。
- 您可以在末尾找到完整的代码示例,以方便复制粘贴 *
问题:
- 您可以在下面找到类型定义和代码示例 *
我试图找出一种方法,如何“组合”(如在函数组合中)多个函数,假设这些函数通过使用附加属性“扩展”单个对象来修改它,将其组合成一个执行所有扩展并正确键入的单个函数。
所讨论的函数是StoreEnhancers<Ext>
(其中Ext
表示扩展结果对象的普通对象),并且它们的合成结果也应该是StoreEnhancer<ExtFinal>
,其中ExtFinal
应该是传递到合成中的每个增强器的所有Ext
的并集。
无论我怎么尝试,使用扩展操作符(...
)传递一个数组,我似乎都无法编写一个组合函数,该函数能够用多个StoreEnhancers
扩展一个对象,并允许typescript推断最终的Ext
,以便我可以访问这些增强器添加的所有属性。
以下是一些定义,以了解更多上下文:
首先,我们可以将StoreEnhancer
定义为接受StoreCreator
或EnahncedStoreCreator
并返回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
// }
1条答案
按热度按时间bt1cpqcv1#
我们的目标是编写一个
composeEnhancers()
函数,它接受可变数量的参数,每个参数都是某个TI
的StoreEnhancer<TI>
值;也就是说,对于某些类型T0
到TN
,自变量将是元组类型[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类型也会产生数组/元组。所以现在我们有
其中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
中所有元素的交集,我们给它一个名字:现在我们需要定义
TupleToIntersection
,这里有一个可能的实现:这是使用这样的特性:如果推理位置处于 * 逆变 * 位置,则条件类型中的类型推理将生成候选类型的交集(参见Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript)比如函数参数,所以我把
T
Map到一个每个元素都是函数参数的版本,把它们组合成一个函数的并集,然后推断出这个函数参数的类型,这就变成了一个交集,这和Transform union type to intersection type中所展示的技术很相似。好了,现在我们有了调用签名。让我们确保调用者看到了所需的行为:
看起来不错;
enhanceWithABC
值是单个StoreEnhancer
,其类型参数是输入StoreEnhancer
s的类型参数的交集。现在我们基本上已经完成了,函数还需要实现,而且实现已经足够简单了,但不幸的是,因为调用签名相当复杂,所以编译器无法验证实现是否真的完全符合调用签名:
这在运行时是可行的,但是编译器并不知道the array
reduce()
method会输出正确类型的a值,它知道你会得到一个EnhancedStoreCreator
,但并不特别涉及TupleToIntersection<T>
,这是TypeScript语言的一个本质上的限制;reduce()
的类型不能足够通用,甚至不能表达从底层循环的开始到结束的类型的渐进变化;参见Typing a reduce over a Typescript tuple。所以最好不要尝试,我们应该努力抑制错误,只是小心地让自己相信我们的实现是正确编写的(因为编译器不能为我们这样做)。
一种方法是删除"turn off typechecking"
any
类型,其中的故障点为:现在没有错误了,这已经是我们能做的最好的了,还有其他方法可以抑制错误,比如类型Assert或者单调用签名重载,但是我不会离题,在这里详细讨论这些方法。
现在,我们已经有了类型和实现,让我们确保
enhanceWithABC()
函数按预期工作:看起来不错!
Playground代码链接