我正在尝试为仿函数映射创建一个通用函数接口(interface),它尊重所提供的接口(interface)。在下面显示的代码中,我想要 mb
的值类型为 Maybe<number>
, 而不是实际类型 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;
}
然而,这不起作用,因为没有类型参数的通用接口(interface)不是有效的 TypeScript 类型,即 Functor
等接口(interface)需要将类型参数视为类型,Functor
本身不是有效类型。
如果目前没有表达这些语义的方法,我们将不胜感激关于需要尽可能少的用户代码的解决方案的任何建议。
提前感谢您的时间和考虑。
最佳答案
阻碍我们的是,当您尝试传递一个类型变量时 F
作为另一个类型变量的类型参数 T
, 比如 T<F>
,即使你知道 T
,TS 也不允许这样做实际上是一个通用接口(interface)。
有一个 discussion关于这个主题可以追溯到 2014 年的一个 github 问题,它仍然是开放的,所以 TS 团队可能不会在不久的将来支持它。
此语言功能的术语称为 higher kinded type .使用该搜索关键字,Google 带我去了一趟兔子洞。
事实证明存在一个非常聪明的解决方法!
利用 TS declaration merging (又名模块扩充)功能,我们可以有效地定义一个空的“类型存储”接口(interface),它就像一个普通对象,包含对其他有用类型的引用。使用这种技术,我们能够克服这个障碍!
我将以您的案例为例来介绍此技术的概念。如果您想深入了解,我在最后提供了一些有用的链接。
这是 TS Playground link (剧透警告)到最终结果。肯定会在现场看到它。现在让我们逐步分解(或者我应该说构建它?)。
- 首先,让我们声明一个空的
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);
引用
关于typescript - 替换通用接口(interface)类型参数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55683711/