You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在TypeScript中编写通用Catamorphism类型与函数

TypeScript联合类型的Catamorphism类型设计问题

我正在尝试为TypeScript中的联合类型设计通用的Catamorphism(余代数同态)类型,但遇到了两个核心问题:

  1. 希望Cata类型是通用的,能适配任意基于Kind的联合类型,而不是固定绑定到某个特定联合类型;
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 19:17:03