替换通用接口类型参数

时间:2019-04-15 06:39:01

标签: typescript typescript-generics

我正在尝试为函子映射创建一个通用的函数接口,该接口尊重所提供的接口。在下面显示的代码中,我希望Application.StatusBar = "Last page was " & i 的值是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);

但是,如果没有类型参数不是有效的TypeScript类型,则它不能用作通用接口,即,诸如// Theoretical Code interface PretendFMap { <A, B, FA extends Functor<A>>(fn: (a: A) => B, Fa: FA): FA extends (infer F)<A> ? F<B> : never; } 之类的接口要求将类型参数视为类型Functor本身不是有效的类型。

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

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

1 个答案:

答案 0 :(得分:4)

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

关于这个主题的discussion可以追溯到2014年的github问题,并且仍然开放,因此TS团队可能在不久的将来不支持它。

此语言功能的术语称为higher kinded type。谷歌用那个搜索关键词带我去了兔子洞。

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

通过利用TS declaration merging(又名模块增强)功能,我们可以有效地定义一个空的“类型存储”接口,该接口的作用就像一个普通对象,该对象持有对其他有用类型的引用。使用这项技术,我们就能克服这种障碍!

我将以您的情况为例介绍这种技术的思想。如果您想更深入地学习,请在最后添加一些有用的链接。

这是最终结果的TS Playground link剧透警报)。可以现场观看。现在,让我们逐步分解它(或者我应该说建立它?)。

  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. 现在,我们使用实用程序类型来修补缺少的语言功能“更高种类的类型”。
// 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. The github discussion on supporting higer kinded type in TS
  2. Entrance to the rabbit hole
  3. Declaration Merging in TS Handbook
  4. SO post on higher kinded type
  5. Medium post by @gcanti, on higher kinded types in TS
  6. fp-ts lib by @gcanti
  7. hkts lib by @pelotom
  8. typeprops lib by @SimonMeskens