原文: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中使用类型推断来模拟类型洞的概念,从而帮助开发者更有效地编写和理解代码。