如何在TypeScript中编写通用Catamorphism类型与函数
TypeScript联合类型的Catamorphism类型设计问题
我正在尝试为TypeScript中的联合类型设计通用的Catamorphism(余代数同态)类型,但遇到了两个核心问题:
- 希望
Cata类型是通用的,能适配任意基于Kind的联合类型,而不是固定绑定到某个特定联合类型; cata函数的类型定义存在错误,无法正确关联联合类型的kind和对应value的类型。
初始尝试代码
首先定义基础的Kind类型和示例联合类型UseAsyncHookState:
type Kind<K extends string, V> = { kind: K, value: V } type UseAsyncHookState<T> = | Kind<"loading", null> | Kind<"error", Error> | Kind<"result", T>
尝试为UseAsyncHookState构建Cata类型,这部分静态类型是正确的:
type Cata<T,Result> = { // 最终生成的类型为: [K in UseAsyncHookState<T> as K["kind"]]: // loading: (value: null) => Result; (value: K["value"]) => Result // error: (value: Error) => Result; } // result: (value: T) => Result; // ✅ 静态类型正确
但cata函数存在类型错误:
function cata<T,Result>(t: T, map: Cata<T,Result>): Result { return map[t.kind](t.value) // ❌ 错误:Element implicitly has an 'any' type because // expression of type 'any' can't be used to // index type 'Cata<T, Result>' }
简化示例:Shape联合类型
用Shape联合类型进一步说明需求:
type Kind<K extends string, V> = { kind: K, value: V } type Shape = | Kind<"circle", { radius: number }> | Kind<"rectangle", { width: number, height: number }> | Kind<"triangle", { base: number, height: number }>
尝试定义通用Cata类型时,出现索引类型错误:
type Cata<T, Result> = { [K in T["kind"]]: // ❌ 错误:Type 'T["kind"]' is not assignable to // type 'string | number | symbol' // Type '"kind"' cannot be used to index type 'T' (value: T["value"]) => Result // ❌ 错误:Type '"value"' cannot be used to index type 'T' }
对应的cata函数同样存在类型错误:
function cata<T, Result>(t: T, map: Cata<T, Result>): Result { return map[t.kind](t.value) // ❌ 同之前的索引类型错误 }
但手动定义的shapeCata是符合预期的:
const shapeCata: Cata<Shape, number> = { circle: ({ radius }) => Math.PI * radius ** 2, rectangle: ({ width, height }) => width * height, triangle: ({ base, height }) => 0.5 * base * height } const circle: Shape = { kind: "circle", value: { radius: 2 } } const rectangle: Shape = { kind: "rectangle", value: { width: 3, height: 4 } } const triangle: Shape = { kind: "triangle", value: { base: 5, height: 6 } } console.log(cata(circle, shapeCata)) // 预期输出12.566... console.log(cata(rectangle, shapeCata)) // 预期输出12 console.log(cata(triangle, shapeCata)) // 预期输出15
尝试2:通用Cata类型的进展
改进后定义了通用的Cata类型,这部分静态类型生成是正确的:
import { ReactNode } from "react" type Kind<K extends string, V> = { kind: K, value: V } type UseAsyncHookState<T> = | Kind<"loading", null> | Kind<"error", Error> | Kind<"result", T> type Shape = | Kind<"circle", { radius: number }> | Kind<"rectangle", { width: number, height: number }> | Kind<"triangle", { base: number, height: number }> type Cata<T extends Kind<string, unknown>, R> = { [U in T as U["kind"]]: (value: U["value"]) => R }
生成的具体Cata类型符合预期:
// ShapeArea类型正确 type ShapeArea = Cata<Shape, number> // 生成的类型: // { // circle: (value: { radius: number; }) => number; // rectangle: (value: { width: number; height: number; }) => number; // triangle: (value: { base: number; height: number; }) => number; // } // AsyncHookComponent类型正确 type AsyncHookComponent<T> = Cata<UseAsyncHookState<T>, ReactNode> // 生成的类型: // { // loading: (value: null) => ReactNode; // error: (value: Error) => ReactNode; // result: (value: T) => ReactNode; // }
但即使为特定联合类型定义单独的cata函数,仍存在类型错误:
function cataAsyncHook<T,R>(t: UseAsyncHookState<T>, map: Cata<UseAsyncHookState<T>, R>): R { return map[t.kind](t.value) // ❌ 错误:Type 'null' is not assignable to type 'never' } function cataShape<R>(t: Shape, map: Cata<Shape, R>): R { return map[t.kind](t.value) // ❌ 错误:Type 'null' is not assignable to type 'never' }
解决方案
要解决cata函数的类型错误,需要让TypeScript能够精确关联联合类型中每个成员的kind和对应的value类型,最简洁的方式是使用泛型约束配合类型断言:
正确的通用cata函数实现
type Kind<K extends string, V> = { kind: K, value: V } type Cata<T extends Kind<string, unknown>, R> = { [U in T as U["kind"]]: (value: U["value"]) => R } function cata<T extends Kind<string, unknown>, R>( t: T, map: Cata<T, R> ): R { // 类型断言告知TS,当前t的kind对应的函数参数与t.value类型匹配 return (map[t.kind] as (value: T["value"]) => R)(t.value) }
如果需要更精确的类型细化,可以通过提取联合成员的方式实现:
function cata<T extends Kind<string, unknown>, R>( t: T, map: Cata<T, R> ): R { type KindKey = T["kind"]; type ValueForKind<K extends KindKey> = Extract<T, { kind: K }>["value"]; return (map[t.kind as KindKey] as (value: ValueForKind<KindKey>) => R)(t.value as ValueForKind<KindKey>) }
验证解决方案
使用Shape示例验证:
const shapeCata: Cata<Shape, number> = { circle: ({ radius }) => Math.PI * radius ** 2, rectangle: ({ width, height }) => width * height, triangle: ({ base, height }) => 0.5 * base * height } const circle: Shape = { kind: "circle", value: { radius: 2 } } console.log(cata(circle, shapeCata)) // 12.566370614359172 ✅ const rectangle: Shape = { kind: "rectangle", value: { width: 3, height: 4 } } console.log(cata(rectangle, shapeCata)) // 12 ✅ const triangle: Shape = { kind: "triangle", value: { base: 5, height: 6 } } console.log(cata(triangle, shapeCata)) // 15 ✅
使用UseAsyncHookState示例验证:
type UseAsyncHookState<T> = | Kind<"loading", null> | Kind<"error", Error> | Kind<"result", T> const asyncStateCata = <T, R>(map: Cata<UseAsyncHookState<T>, R>) => (state: UseAsyncHookState<T>) => cata(state, map) const loadingState: UseAsyncHookState<string> = { kind: "loading", value: null } const errorState: UseAsyncHookState<string> = { kind: "error", value: new Error("Failed") } const resultState: UseAsyncHookState<string> = { kind: "result", value: "Success" } const stateToString = asyncStateCata<string, string>({ loading: () => "Loading...", error: (err) => `Error: ${err.message}`, result: (val) => `Result: ${val}` }) console.log(stateToString(loadingState)) // "Loading..." ✅ console.log(stateToString(errorState)) // "Error: Failed" ✅ console.log(stateToString(resultState)) // "Result: Success" ✅
内容的提问来源于stack exchange,提问作者Mulan
相关产品推荐
相关产品推荐

