typescript - 替换通用接口类型参数
问题描述
我正在尝试为仿函数映射创建一个通用函数接口,它尊重所提供的接口。在下面显示的代码中,我希望 的值为mb
type Maybe<number>
,而不是实际的 type Functor<number>
。
我确实意识到一种可能的解决方案是向 interface 添加重载FMap
。我对这个解决方案不满意的原因是我希望这个代码驻留在一个包中,允许用户创建实现Functor
,并在使用函数时具有我上面描述的行为map
。
interface Functor<A> {
map<B>(fn: (a: A) => B): Functor<B>;
}
interface FMap {
<A, B>(fn: (a: A) => B, Fa: Functor<A>): Functor<B>;
}
const map: FMap = (fn, Fa) => (
Fa.map(fn)
);
class Maybe<A> implements Functor<A> {
constructor(private readonly a: A) {}
map<B>(fn: (a: A) => B): Maybe<B> {
return new Maybe<B>(fn(this.a));
}
}
const sqr = (x: number) => x*x;
const ma = new Maybe(5);
const mb = map(sqr, ma);
我想要一些表达以下语义的方法:
// Theoretical Code
interface PretendFMap {
<A, B, FA extends Functor<A>>(fn: (a: A) => B, Fa: FA): FA extends (infer F)<A> ? F<B> : never;
}
然而,这不起作用,因为没有类型参数的泛型接口不是有效的 TypeScript 类型,即接口Functor
需要类型参数才能被视为类型,Functor
它本身不是有效类型。
如果目前没有表达这些语义的方法,任何关于在用户方面需要尽可能少代码的解决方案的建议将不胜感激。
提前感谢您的时间和考虑。
解决方案
阻碍我们的是,当您尝试将类型变量F
作为类型参数传递给另一个类型变量T
时,例如,即使您知道实际上是泛型接口T<F>
,TS 也不允许这样做。T
在 2014 年的一个 github issue 中有关于这个话题的讨论,并且它仍然是开放的,所以 TS 团队可能在不久的将来不会支持它。
这种语言特征的术语称为高级类型。使用该搜索关键字,谷歌带我去兔子洞旅行。
事实证明存在一个非常聪明的解决方法!
通过利用 TS声明合并(又名模块扩充)特性,我们可以有效地定义一个空的“类型存储”接口,它就像一个普通对象,包含对其他有用类型的引用。使用这种技术,我们能够克服这个障碍!
我将以您的案例为例来介绍这种技术的想法。如果您想深入了解,我会在最后提供一些有用的链接。
这是最终结果的TS Playground 链接(剧透警告)。肯定会在现场看到它。现在让我们一步一步地分解它(或者我应该说建立它?)。
- 首先,让我们声明一个空
TypeStore
接口,我们稍后会更新它的内容。
// just think of it as a plain object
interface TypeStore<A> { } // why '<A>'? see below
// example of "declaration merging"
// it's not re-declaring the same interface
// but just adding new members to the interface
// so we can amend-update the interface dynamically
interface TypeStore<A> {
Foo: Whatever<A>;
Maybe: Maybe<A>;
}
- 让我们也得到
keyof TypeStore
. 请注意,随着内容的TypeStore
更新,$keys
也会相应地更新。
type $keys = keyof TypeStore<any>
- 现在我们使用实用程序类型来修补缺失的语言特性“higher kinded type”。
// the '$' generic param is not just `string` but `string literal`
// think of it as a unique symbol
type HKT<$ extends $keys, A> = TypeStore<A>[$]
// where we mean `Maybe<A>`
// we can instead use:
HKT<'Maybe', A> // again, 'Maybe' is not string type, it's string literal
- 现在我们有了正确的工具,让我们开始构建有用的东西。
interface Functor<$ extends $keys, A> {
map<B>(f: (a: A) => B): HKT<$, B>
}
class Maybe<A> implements Functor<'Maybe', A> {
constructor(private readonly a: A) {}
map<B>(f: (a: A) => B): HKT<'Maybe', B> {
return new Maybe(f(this.a));
}
}
// HERE's the key!
// You put the freshly declare class back into `TypeStore`
// and give it a string literal key 'Maybe'
interface TypeStore<A> {
Maybe: Maybe<A>
}
- 最后
FMap
:
// `infer $` is the key here
// remember what blocked us?
// we cannot "infer Maybe from T" then apply "Maybe<A>"
// but we can "infer $" then apply "HKT<$, A>"!
interface FMap {
<A, B, FA extends { map: Function }>
(f: (a: A) => B, fa: FA): FA extends HKT<infer $, A> ? HKT<$, B> : any
}
const map: FMap = (fn, Fa) => Fa.map(fn);
参考
推荐阅读
- python-3.x - 如何在 pymongo 中为“IN”子句查询包含 NULL 或 None 值?
- python - 如何使用 NUMBA 加速我的 python 代码?
- java - 指定主类的路径并使用 sbt 文件包含库
- ruby-on-rails - 计算特定用户通过 Active Storage 下载文件的次数
- reactjs - 从反应导航中获取 mapStateToProps 路由参数
- javascript - 导航时在菜单项上加下划线
- regex - 匹配 if - else if - else 阶梯
- vue.js - 如何将 Vuex 命名空间的 getter 与 Composition API 一起使用
- android - 正则表达式 NOT [^ ... ] 表达式不适用于复杂表达式
- openlayers - 使用饼图进行地图聚类以使用开放层可视化比率