替换通用接口类型参数

McP*_*ott 5 typescript typescript-generics

我正在尝试为函子映射创建一个通用的函数接口,该接口尊重所提供的接口。在下面显示的代码中,我希望value mb的类型Maybe<number>为type ,而不是实际的type 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);
Run Code Online (Sandbox Code Playgroud)

我想要一些表达以下语义的方法:

// Theoretical Code

interface PretendFMap {
  <A, B, FA extends Functor<A>>(fn: (a: A) => B, Fa: FA): FA extends (infer F)<A> ? F<B> : never;
}
Run Code Online (Sandbox Code Playgroud)

但是,如果没有类型参数,则它不能用作通用接口,不是有效的TypeScript类型,即,诸如Functor要求将类型参数视为类型的接口,Functor其本身不是有效类型。

如果当前没有表达这些语义的手段,那么与解决方案有关的任何建议在用户方面都需要尽可能少的代码,将受到极大的赞赏。

预先感谢您的时间和考虑。

hac*_*ape 5

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

关于这个话题的讨论可以追溯到2014年的github问题,目前仍在公开讨论中,因此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>;
}
Run Code Online (Sandbox Code Playgroud)
  1. 让我们也获得keyof TypeStore。注意,随着内容的TypeStore更新,$keys也会相应地更新。
type $keys = keyof TypeStore<any>
Run Code Online (Sandbox Code Playgroud)
  1. 现在,我们使用实用程序类型来修补缺少的语言功能“更高种类的类型”。
// 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
Run Code Online (Sandbox Code Playgroud)
  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>
}
Run Code Online (Sandbox Code Playgroud)
  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);
Run Code Online (Sandbox Code Playgroud)

参考

  1. 有关在TS中支持更高种类类型的github讨论
  2. 兔子洞入口
  3. TS手册中的声明合并
  4. 所以在更高种类的类型上发布
  5. @gcanti的中级帖子,有关TS中更高种类的类型
  6. fp-ts lib由@gcanti
  7. hkts 由@pelotom lib
  8. typeprops lib由@SimonMeskens