TypeScript 中的 HKT 问题
理解为什么泛型类型构造器难以实现。
TypeScript 中的 HKT 问题 是 CoddyKit 上的免费 TypeScript Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 TypeScript Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 TypeScript Academy 课程共包含 4 节课。
什么是高阶类型
高阶类型(HKT)抽象的不是具体类型,而是类型构造器:例如 Array 或 Promise,它们需要一个参数才能成为真正的类型。
单独的 Array 不是类型,Array<string> 才是。Array 是作用于类型的函数,而 HKT 让我们可以对这类函数进行泛型处理。
type StringArray = Array<string>;
type NumberArray = Array<number>;
// Array by itself is a type constructor, not a type简述类型种类
正如值具有类型一样,类型构造器也具有种类。像 number 这样的普通类型的种类是 *。Array 的种类是 * -> *:给它一个类型,就会得到一个类型。对这类构造器进行泛型处理,就是“高阶”的含义。
type ValueLike = number; // kind *
// Array needs one argument: kind * -> *
// Map needs two arguments: kind * -> * -> *目标:Functor<F>
在支持 HKT 的语言中,您可以编写一个统一的 Functor<F> 抽象,其中包含一个适用于任意容器 F 的 map,无论它是数组、选项还是承诺类型。一个接口,多个容器。
interface Wish_Functor<F> {
map<A, B>(fa: F<A>, f: (a: A) => B): F<B>;
}
// F<A> here is the wish - and TypeScript rejects it问题所在
TypeScript 不支持将类型参数应用到参数上。当 F 是泛型参数时,您无法编写 F<A>。编译器会报错:“类型 F 不是泛型”。
// interface Functor<F> {
// map<A, B>(fa: F<A>, f: (a: A) => B): F<B>;
// }
// Error: Type F is not generic为什么 TS 不支持它
TypeScript 类型参数是一阶的:它们表示具体类型,而不是类型构造器。没有语法可以表达“F 是一个接收一个类型参数的东西”。因此,F<A> 对编译器来说没有意义。
// You can pass number, string, User as F...
// but not Array, not Promise (the constructors themselves)一个具体的痛点
没有 HKT,您必须为每种容器重复实现 map:数组需要一个,选项需要一个,承诺也需要一个。逻辑完全相同,但类型系统无法将它们统一起来。
declare function mapArray<A, B>(fa: A[], f: (a: A) => B): B[];
declare function mapOption<A, B>(fa: A | null, f: (a: A) => B): B | null;
// Same shape, but no way to abstract over the container我们想表达什么
我们希望使用一个签名:给定一个装有 A 的容器和一个 A -> B 函数,返回一个装有 B 的容器,同时保留容器类型。数组仍然是数组,选项仍然是选项。
// Wish: map<F, A, B>(fa: F<A>, f: (a: A) => B): F<B>
// where F is preserved across the call其他语言
Haskell 和 Scala 原生支持 HKT,因此可以在其中表达 Functor、Applicative 和 Monad 这样的类型类。TypeScript 需要使用变通方案来近似实现同等能力。
// Haskell: class Functor f where fmap :: (a -> b) -> f a -> f b
// TypeScript: needs an encoding trick (next lessons)编码思路
标准的变通方案是去函数化:我们不直接应用 F,而是为每种容器提供一个字符串标签(统一资源标识符),并维护一个注册表,将标签和参数映射到具体类型。类型应用就变成了一次查找。
// Instead of F<A>, we write Kind<F, A> where F is a URI string
// and a registry resolves it to the real type您将构建什么
接下来的课程将逐步构建 fp-ts 风格的 HKT 编码:一个 URItoKind 注册表、一次 Kind 查找,以及一个最终能够让 map 作用于任意已注册容器的泛型 Functor。
// Roadmap:
// 1. Type constructors via URI strings
// 2. Kind<F, A> resolution
// 3. Generic Functor + instances为什么值得了解
您很少需要亲自编写这种编码,但 fp-ts 和 Effect 等库都依赖它。理解这一技巧后,您就能读懂它们的类型,针对容器编写泛型抽象,并理解类型系统能够做到什么、又无法做到什么。
// Recognizing Kind<F, A> in library code is the payoff快速检查
测试您对 HKT 问题的理解。
回顾
您了解了高阶类型问题。
- HKT 抽象的是像
Array这样的类型构造器,而不是具体类型。 - TypeScript 的参数是一阶的,因此
F<A>是非法的。 - 这使得使用一个
map编写单一泛型Functor成为不可能。 - 解决方法是使用统一资源标识符标签和注册表进行编码。
下一步:通过统一资源标识符字符串定义类型构造器。
常见问题解答
「TypeScript 中的 HKT 问题」课时是免费的吗?
是的 — 「TypeScript 中的 HKT 问题」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 TypeScript Academy 课程的其余内容,请升级到 CoddyKit PRO。 TypeScript Academy 课程共包含 4 节课。
「TypeScript 中的 HKT 问题」这节课中我会学到什么?
理解为什么泛型类型构造器难以实现。 你通过在浏览器中直接运行的动手代码来练习 TypeScript Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 TypeScript Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 TypeScript Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「TypeScript 中的 HKT 问题」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 TypeScript Academy 课中编写并运行代码吗?
能。每节 TypeScript Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- TypeScript 中的 HKT 问题
- 定义类型构造器
- 轻量级 HKT 模式
- 泛型函子与映射器