首页 > 解决方案 > 替换通用接口类型参数

问题描述

我正在尝试为仿函数映射创建一个通用函数接口,它尊重所提供的接口。在下面显示的代码中,我希望 的值为mbtype 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它本身不是有效类型。

如果目前没有表达这些语义的方法,任何关于在用户方面需要尽可能少代码的解决方案的建议将不胜感激。

提前感谢您的时间和考虑。

标签: typescripttypescript-generics

解决方案


阻碍我们的是,当您尝试将类型变量F作为类型参数传递给另一个类型变量T时,例如,即使您知道实际上是泛型接口T<F>,TS 也不允许这样做。T

在 2014 年的一个 github issue 中有关于这个话题的讨论,并且它仍然是开放的,所以 TS 团队可能在不久的将来不会支持它。

这种语言特征的术语称为高级类型。使用该搜索关键字,谷歌带我去兔子洞旅行。

事实证明存在一个非常聪明的解决方法!

通过利用 TS声明合并(又名模块扩充)特性,我们可以有效地定义一个空的“类型存储”接口,它就像一个普通对象,包含对其他有用类型的引用。使用这种技术,我们能够克服这个障碍!

我将以您的案例为例来介绍这种技术的想法。如果您想深入了解,我会在最后提供一些有用的链接。

这是最终结果的TS Playground 链接剧透警告)。肯定会在现场看到它。现在让我们一步一步地分解它(或者我应该说建立它?)。

  1. 首先,让我们声明一个空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>;
}
  1. 让我们也得到keyof TypeStore. 请注意,随着内容的TypeStore更新,$keys也会相应地更新。
type $keys = keyof TypeStore<any>
  1. 现在我们使用实用程序类型来修补缺失的语言特性“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
  1. 现在我们有了正确的工具,让我们开始构建有用的东西。
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>
}
  1. 最后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);

参考

  1. github关于TS中支持higer kinded type的讨论
  2. 兔子洞的入口
  3. TS手册中的声明合并
  4. 所以在更高种类的类型上发帖
  5. @gcanti 发表的中篇文章,关于 TS 中更高种类的类型
  6. fp-ts@gcanti 的库
  7. hkts@pelotom 的库
  8. typeprops@SimonMeskens 的库

推荐阅读