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

如何为非函数参数的动态增长对象添加TypeScript类型定义?

类型安全的动态步骤库改造方案

场景说明

我有一个提供辅助函数的库,用来动态扩展和使用对象,还有若干文件借助该辅助函数定义对象的处理步骤:

// samplestepdefinition.ts
interface C {
  c1: string,
  c2: string
}

const isGreaterThanTwo = (number: number): boolean => number > 2

const steps = [
  Steps.initialize('a', 3),
  Steps.provide('b', 'a', isGreaterThanTwo),
  Steps.provide('c', 'b', (b: boolean): C => ({ c1: 'foo', c2: b ? 'bar' : 'baz' })),
  Steps.use('c.c2', console.log)
]
Steps.processSteps(steps) // logs 'bar'
// stepslib-simplified.ts
import lodash_get from 'lodash/fp/get'
import lodash_set from 'lodash/fp/set'

interface StepContext extends Object { }
type StepResolver = (context: StepContext) => StepContext // 实际返回的是修改后的上下文

type ProviderFn = (valueToUse: any) => any
type Provide = (keyToSet: string, keyToUse: string, fn: ProviderFn) => StepResolver
const provide: Provide = (keyToSet, keyToUse, fn) => (context) =>
  lodash_set(keyToSet, fn(lodash_get(keyToUse, context)), context)

type Initialize = (keyToSet: string, value: any) => StepResolver
const initialize: Initialize = lodash_set

type UserFn = (valueToUse: any) => any
type Use = (keyToUse: string, fn: UserFn) => StepResolver
const use: Use = (keyToUse, fn) => context => fn(lodash_get(keyToUse, context))

const Steps = {
  provide,
  initialize,
  use,
  processSteps: (steps: StepResolver[]): void => {
    steps.reduce((prevContext, step) => step(prevContext), {})
  }
}
export default Steps

问题挑战

当前示例完全没有类型校验:比如把isGreaterThanTwo的参数改成字符串类型,或者第三步写成Steps.provide('c', 'b', (b: number): [...],代码都不会报错。

理想方案

能否仅修改stepslib,让它自动推断所有类型,让StepResolver返回包含新增键值的复合类型?我自行尝试后未找到实现方法。

备选方案

如果无法实现自动推断,能不能让stepdefinition文件将如下自定义泛型传入processSteps以实现类型校验?

interface SpecificContext extends StepContext {
  a?: number
  b?: boolean
  c?: C
}

我修改stepslib后遇到类型错误:Argument of type '{}' is not assignable to parameter of type 'Context',求解决办法。

注:可先忽略'c.c2'这类嵌套键场景,优先解决直接键的类型问题。


解决方案

一、理想方案:自动推断类型的实现

通过TypeScript泛型和类型断言,让每个步骤函数追踪上下文的类型变化,最终实现全链路类型校验。修改后的stepslib-simplified.ts如下:

import lodash_get from 'lodash/fp/get'
import lodash_set from 'lodash/fp/set'

type StepContext = {}

// 步骤解析器:接收输入上下文Ctx,返回新增键后的上下文
type StepResolver<Ctx extends StepContext, OutputCtx extends StepContext = Ctx> = (context: Ctx) => OutputCtx

// Initialize:新增键值对,泛型约束键名和值类型
type Initialize = <K extends string, V>(keyToSet: K, value: V) => StepResolver<StepContext, StepContext & { [key in K]: V }>
const initialize: Initialize = <K extends string, V>(keyToSet: K, value: V) => 
  (context) => lodash_set(keyToSet, value, context) as StepContext & { [key in K]: V }

// Provide:基于已有键生成新键,自动推断依赖键的类型
type Provide = <Ctx extends StepContext, K extends keyof Ctx, NewK extends string, V>(
  keyToSet: NewK,
  keyToUse: K,
  fn: (value: Ctx[K]) => V
) => StepResolver<Ctx, Ctx & { [key in NewK]: V }>
const provide: Provide = <Ctx extends StepContext, K extends keyof Ctx, NewK extends string, V>(
  keyToSet: NewK,
  keyToUse: K,
  fn: (value: Ctx[K]) => V
) => (context) => 
  lodash_set(keyToSet, fn(lodash_get(keyToUse, context)), context) as Ctx & { [key in NewK]: V }

// Use:使用已有键的值,约束回调参数类型
type Use = <Ctx extends StepContext, K extends keyof Ctx>(
  keyToUse: K,
  fn: (value: Ctx[K]) => void
) => StepResolver<Ctx>
const use: Use = <Ctx extends StepContext, K extends keyof Ctx>(keyToUse: K, fn: (value: Ctx[K]) => void) => 
  (context) => { fn(lodash_get(keyToUse, context)); return context }

// processSteps:自动推断步骤链的上下文类型变化
const processSteps = <Ctx extends StepContext>(steps: Array<StepResolver<any, Ctx>>): void => {
  steps.reduce((prevContext, step) => step(prevContext), {} as StepContext)
}

const Steps = {
  provide,
  initialize,
  use,
  processSteps
}
export default Steps

改造后效果:

  • 步骤链的上下文类型会被自动追踪,比如initialize('a',3)会生成{a: number}的上下文类型
  • 后续步骤的依赖键类型会被自动推断,若isGreaterThanTwo参数改为string,或第三步回调参数写成number,TypeScript会直接报错
  • 完全不需要在业务侧手动声明上下文类型,实现无侵入的类型安全

二、备选方案:手动传入上下文类型的实现

如果不需要自动推断,可通过自定义泛型约束上下文,修改stepslib解决空对象类型错误:

import lodash_get from 'lodash/fp/get'
import lodash_set from 'lodash/fp/set'

interface StepContext {}

// 基于自定义上下文的步骤解析器
type StepResolver<Ctx extends StepContext> = (context: Ctx) => Ctx

// Initialize:约束键和值匹配自定义上下文
type Initialize<Ctx extends StepContext> = <K extends keyof Ctx>(keyToSet: K, value: Ctx[K]) => StepResolver<Ctx>
const initialize: Initialize<any> = <K extends string, V>(keyToSet: K, value: V) => 
  (context) => lodash_set(keyToSet, value, context)

// Provide:约束依赖键和新增键都在自定义上下文中
type Provide<Ctx extends StepContext> = <K extends keyof Ctx, NewK extends keyof Ctx>(
  keyToSet: NewK,
  keyToUse: K,
  fn: (value: Ctx[K]) => Ctx[NewK]
) => StepResolver<Ctx>
const provide: Provide<any> = <K extends string, NewK extends string, V>(
  keyToSet: NewK,
  keyToUse: K,
  fn: (value: any) => V
) => (context) => 
  lodash_set(keyToSet, fn(lodash_get(keyToUse, context)), context)

// Use:约束使用的键存在于自定义上下文中
type Use<Ctx extends StepContext> = <K extends keyof Ctx>(
  keyToUse: K,
  fn: (value: Ctx[K]) => void
) => StepResolver<Ctx>
const use: Use<any> = <K extends string>(keyToUse: K, fn: (value: any) => void) => 
  (context) => { fn(lodash_get(keyToUse, context)); return context }

// processSteps:用Partial<Ctx>兼容初始空对象,再断言为Ctx
const processSteps = <Ctx extends StepContext>(steps: StepResolver<Ctx>[]): void => {
  steps.reduce((prevContext, step) => step(prevContext), {} as Partial<Ctx> as Ctx)
}

const Steps = {
  provide,
  initialize,
  use,
  processSteps
}
export default Steps

业务侧使用方式:

interface C {
  c1: string,
  c2: string
}

interface SpecificContext extends StepContext {
  a?: number
  b?: boolean
  c?: C
}

const isGreaterThanTwo = (number: number): boolean => number > 2

const steps: Array<StepResolver<SpecificContext>> = [
  Steps.initialize('a', 3),
  Steps.provide('b', 'a', isGreaterThanTwo),
  Steps.provide('c', 'b', (b: boolean): C => ({ c1: 'foo', c2: b ? 'bar' : 'baz' })),
  Steps.use('c', ctx => console.log(ctx.c2))
]
Steps.processSteps<SpecificContext>(steps)

核心解决点:将初始上下文声明为Partial<Ctx> as Ctx,因为空对象{}符合所有键可选的Partial<SpecificContext>,再通过类型断言转换为目标上下文类型,解决空对象赋值的类型错误。


内容的提问来源于stack exchange,提问作者KonstantinK

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 16:05:35