替换通用接口类型参数
Posted
技术标签:
【中文标题】替换通用接口类型参数【英文标题】:Replace Generic Interface Type Parameter 【发布时间】:2021-01-10 11:16:39 【问题描述】:我正在尝试为仿函数映射创建一个通用函数接口,它尊重所提供的接口。在下面显示的代码中,我希望 mb
的值是 Maybe<number>
类型,而不是实际类型 Functor<number>
。
我确实意识到一种可能的解决方案是向接口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
被推断为什么?
我希望在 map(sqr, ma)
的情况下推断为 Maybe
。但这显然行不通,因为Maybe
本身不是一种类型。我想要的是保留提供给map
的具体实现,而不是返回Functor
。
有一个讨论here,现在可能没办法正常了。
@hackape 非常感谢这个链接!我一直在搜索这些问题,但没有合适的词汇来找到任何有用的东西。
嗯,看起来值得转发作为答案。
【参考方案1】:
阻碍我们的是,当您尝试将类型变量 F
作为类型参数传递给另一个类型变量 T
时,例如 T<F>
,即使您知道 @987654336,TS 也不允许这样做@ 实际上是一个通用接口。
在 2014 年的一个 github issue 中有一个关于这个主题的 discussion,它仍然是开放的,所以 TS 团队在不久的将来可能不会支持它。
此语言功能的术语称为higher kinded type。使用该搜索关键字,谷歌带我去了一次兔子洞。
事实证明存在一个非常聪明的解决方法!
通过利用 TS declaration merging(又名 模块扩充)特性,我们可以有效地定义一个空的“类型存储”接口,它的作用就像一个保存对其他有用类型的引用的普通对象。使用这种技术,我们能够克服这个障碍!
我将以您的案例为例来介绍这种技术的想法。如果您想深入了解,我会在最后提供一些有用的链接。
这是最终结果的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>
-
现在我们使用实用程序类型来修补缺失的语言功能“高级类型”。
// 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);
参考
-
The github discussion on supporting higer kinded type in TS
Entrance to the rabbit hole
Declaration Merging in TS Handbook
SO post on higher kinded type
Medium post by @gcanti, on higher kinded types in TS
fp-ts
lib by @gcanti
hkts
lib by @pelotom
typeprops
lib by @SimonMeskens
【讨论】:
以上是关于替换通用接口类型参数的主要内容,如果未能解决你的问题,请参考以下文章