原文:https://dev.to/gcanti/type-holes-in-typescript-2lck
这篇文章的标题是《Type holes in TypeScript》,由Giulio Canti在2019年6月28日发表,并在6月29日更新。文章讨论了如何在TypeScript中模拟“类型洞”的概念,这是一种编程技巧,通过编写函数的一部分,然后利用编译器来帮助完成其余部分。这个过程是迭代的,可以看作是与编译器的对话。通过这种方式,开发者可以逐步接近正确的答案,最终可能在不太了解具体如何实现的情况下完成函数的编写。
什么是类型洞?
类型洞的概念是先实现你已知如何完成的函数的一小部分,然后请求编译器帮助完成其余部分。这是一个迭代的过程,是与编译器的对话。在这个过程中,每一步都让你更接近正确的答案,经过足够的迭代后,你的函数几乎可以自我完成——即使你并不完全清楚它是如何实现的。
TypeScript不支持类型洞,但可以进行某种程度的模拟。
第一个例子
文章首先展示了一个函数声明的例子:
declare function jonk<A, B>(
ab: (a: A) => B,
ann: (an: (a: A) => number) => number
): (bn: (b: B) => number) => number为了模拟类型洞,作者使用了一个通用的函数声明:
declare function _<T>(): T将这个函数放入jonk函数体中:
function jonk<A, B>(
ab: (a: A) => B,
ann: (an: (a: A) => number) => number
): (bn: (b: B) => number) => number {
return _()
}如果你将鼠标悬停在“类型洞”_上,你可以看到TypeScript推断的类型参数T:
(bn: (b: B) => number) => number所以,类型检查器告诉我们两件事:
我们想要替换
_()的表达式必须具有类型(bn: (b: B) => number) => number。我们有一些局部绑定(
ab、ann、jonk及其类型),我们可以使用它们来帮助实现。
由于我们的洞具有类型(bn: (b: B) => number) => number,我们应该在lambda中绑定bn(从现在起,我将只写函数体):
return bn => _() // 推断类型:number新的推断类型是什么?number。我们如何产生一个number?我们可以使用ab、ann或bn。由于ann和bn都返回一个number,让我们选择ann作为猜测:
return bn => ann(_()) // 推断类型:(a: A) => number我们的新洞具有函数类型,所以让我们引入一个lambda:
return bn => ann(a => _()) // 推断类型:number我们需要再次产生一个number,这次让我们选择bn:
return bn => ann(a => bn(_())) // 推断类型:B现在我们需要产生一个B。我们有一个函数可以做到这一点,ab: (a: A) => B:
return bn => ann(a => bn(ab(_()))) // 推断类型:A最后,我们有一个洞,其类型是A。由于我们有一个A(a参数),让我们直接使用它:
function jonk<A, B>(
ab: (a: A) => B,
ann: (an: (a: A) => number) => number
): (bnn: (b: B) => number) => number {
return bn => ann(a => bn(ab(a)))
}现在我们几乎完全由类型检查器驱动地完成了实现。
第二个例子
让我们来处理博客文章中的第二个例子:zoop:
declare function zoop<A, B>(abb: (a: A) => (b: B) => B, b: B, as: Array<A>): B我们注意到as的类型是Array<A>,让我们使用foldLeft来“模式匹配”它:
import { foldLeft } from 'fp-ts/lib/Array'
import { pipe } from 'fp-ts/lib/pipeable'
function zoop<A, B>(abb: (a: A) => (b: B) => B, b: B, as: Array<A>): B {
return pipe(
as,
foldLeft(
() => _(), // 推断类型:B
(head, tail) => _() // 推断类型:B
)
)
}我们需要为“空”情况(即数组为空时)产生一个B。由于我们有一个B,让我们直接使用它(从现在起,我将只写函数体):
return pipe(
as,
foldLeft(
() => b,
(head, tail) => _() // 推断类型:B
)
)再次,我们想要为其他情况产生一个B,我们想要调用abb。由于它接受两个参数,让我们给它两个洞:
return pipe(
as,
foldLeft(
() => b,
(head, tail) =>
abb(
_() // 推断类型:A
)(
_() // 推断类型:B
)
)
)head的类型是A,所以让我们使用它:
return pipe(
as,
foldLeft(
() => b,
(head, tail) =>
abb(head)(
_() // 推断类型:B
)
)
)现在我们必须产生一个B,我们想要使用tail,它具有类型Array<A>。我们唯一的选择是使用zoop本身:
function zoop<A, B>(abb: (a: A) => (b: B) => B, b: B, as: Array<A>): B {
return pipe(
as,
foldLeft(() => b, (head, tail) => abb(head)(zoop(abb, b, tail)))
)
}
// p.s. `zoop`是`reduceRight`这就是文章的主要内容。文章通过两个例子展示了如何在TypeScript中使用类型推断来模拟类型洞的概念,从而帮助开发者更有效地编写和理解代码。