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

如何移除TS中的any并让getValue推断Schema键?配置工具多项目复用

如何移除TypeScript中的"any"类型,让TS自动推断Schema的键名?

问题背景

我正在重构一个供多子项目共享的配置工具库(发布为@example/example),目标是让getValue函数具备键名自动补全能力。当前代码使用了any类型,想要替换掉;同时希望每个子项目初始化配置后,整个项目中调用getValue时,TS能自动识别传入Schema的键。

每个子项目处于独立环境,另一种思路是给每个子项目单独编写适配器,这种方案可行吗?

此外,我尝试用unknown代替any,但无法给变量重新赋值,这是为什么?如果这是TypeScript的特性限制,能否解释原因并提供更DRY的最佳实践?

原始库代码

// Minimal set and get configuration provider functionalities, pass in a schema and get by key
interface ConvictLike<T> {
  get: (keyName: keyof T) => T[keyof T] | undefined
  validate: () => void
}
type Schema<T> = {
  [P in keyof T]: T[P]
}

const convictLike = <T>(obj: Schema<T>): ConvictLike<T> => {
  return {
    get: (keyName: keyof T) => {
      if (obj && typeof obj === 'object') {
        return keyName in obj ? obj[keyName as keyof T] : undefined
      }
      throw new Error('Invalid object')
    },
    validate: () => {},
  }
}

// TODO so get can infer keys
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let configurationProvider: ConvictLike<any> | undefined

export function initializeAndValidate<T>(schema: T) {
  configurationProvider = convictLike<T>(schema)
  configurationProvider.validate()
}

export function getValue(keyName: string) {
  if (!configurationProvider) {
    throw new Error('Configuration has not been initialized yet')
  }
  return configurationProvider.get(keyName)
}

子项目使用示例

import { getValue, initializeAndValidate } from './provider' // or '@example/example'

initializeAndValidate({ test: { doc: 'test', format: String, default: 'test' } })

console.log(getValue('test'))

为什么unknown代替any会赋值失败?

unknown是TypeScript中最严格的顶层类型,它的兼容性规则和any完全不同:

  • any可以和任意类型双向兼容,相当于关闭了类型检查;
  • unknown只能被赋值给unknown或any类型,反过来,其他类型不能直接赋值给unknown。

当你把configurationProvider声明为ConvictLike<unknown> | undefined时,convictLike<T>(schema)返回的ConvictLike<T>无法赋值给它——因为TS无法确认具体的T类型是否和unknown兼容,这种场景下unknown的严格性反而成为了障碍。


解决方案与最佳实践

方案1:工厂函数模式(推荐,DRY且适配多子项目)

将原库改成工厂模式,让每个子项目生成独立的配置实例,实例的类型会绑定到当前子项目的Schema,完全消除any:

// provider.ts(修改后的库代码)
interface ConvictLike<T> {
  get: (keyName: keyof T) => T[keyof T] | undefined
  validate: () => void
}
type Schema<T> = { [P in keyof T]: T[P] }

const createConvictLike = <T>(obj: Schema<T>): ConvictLike<T> => {
  return {
    get: (keyName: keyof T) => {
      if (obj && typeof obj === 'object') {
        return keyName in obj ? obj[keyName as keyof T] : undefined
      }
      throw new Error('Invalid object')
    },
    validate: () => {},
  }
}

export function createConfigurationProvider<T>() {
  let provider: ConvictLike<T> | undefined

  return {
    initializeAndValidate: (schema: T) => {
      provider = createConvictLike(schema)
      provider.validate()
    },
    getValue: (keyName: keyof T) => {
      if (!provider) {
        throw new Error('Configuration has not been initialized yet')
      }
      return provider.get(keyName)
    }
  }
}

子项目使用方式:

// 子项目代码
import { createConfigurationProvider } from '@example/example'

// 声明当前子项目的Schema类型
type AppConfig = {
  test: { doc: string; format: typeof String; default: string }
}

// 生成当前项目专属的配置工具
const { initializeAndValidate, getValue } = createConfigurationProvider<AppConfig>()

initializeAndValidate({ test: { doc: 'test', format: String, default: 'test' } })

console.log(getValue('test')) // 这里会自动补全键名,且类型安全

这种方式让每个子项目的配置类型完全独立,不需要重复编写冗余代码,是最符合DRY原则的方案。

方案2:子项目单独编写适配器(可行,无需修改原库)

如果无法修改原库,每个子项目可以自己封装一层适配器,显式声明Schema类型,实现类型安全:

// 子项目的config/adapter.ts
import { initializeAndValidate as baseInit, getValue as baseGet } from '@example/example'

// 声明当前子项目的Schema类型
type AppConfig = {
  test: { doc: string; format: typeof String; default: string }
}

export const initializeAndValidate = (schema: AppConfig) => baseInit(schema)
export const getValue = (keyName: keyof AppConfig) => baseGet(keyName) as AppConfig[keyof AppConfig]

子项目中直接导入自己的适配器即可,虽然每个子项目要写重复代码,但实现简单,能满足类型安全和自动补全的需求。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 22:35:02