【问题标题】:How do I eliminate union with undefined in partial index type?如何消除部分索引类型中未定义的联合?
【发布时间】:2026-01-31 09:15:01
【问题描述】:

我有一个objectMap() 函数,它接受一个对象,将一个函数应用于每个值,并返回一个具有相同键但值不同的对象。我为它写了以下声明:

declare function objectMap<TInputObject, TOutputValue>(
        target: TInputObject,
        callback: (currentValue: TInputObject[keyof TInputObject]) => TOutputValue,
    ): {[k in keyof TInputObject]: TOutputValue};

我希望这会使用在对象值中找到的类型调用回调函数,并返回一个与输入对象具有相同键结构的对象。这适用于大多数类型的对象,例如简单的映射,甚至字符串文字的联合:

type in1 = {[k: string]: string}

type keys = 'a' | 'b' | 'c'
type in2 = {[k in keys]: number}

但它会被字符串文字联合类型的可选键分解:

type keys = 'a' | 'b' | 'c'
type in = {[key in keys]?: number }

在这种情况下,当我尝试使用该功能时:

const obj : {[key in keys]?: number} = {a: 1, b: 2}
const result = objectMap(obj, x => x+1)

变量x 的类型为number | undefined(即使所有存在的键都有一个实际的数值),而result 除了可选键之外,最终还有未定义的值。

如何更改objectMap() 的声明以便获得预期结果,其中x 只是numberresult{[k in keys]?: number

【问题讨论】:

  • {a?: number}{a?: number | undefined} 类型是相同的,假设您启用了 --strictNullChecks。在 TypeScript 中(从 TS3.5 开始)并不总是可以distinguish a missing property from one which is present with an undefined value
  • 另外,请尝试将此代码编辑为 minimal reproducible example,以演示您所看到的问题。就目前而言,objectMap({a: 1, b: 2}, x =&gt; x + 1) 没有您描述的问题; x 被推断为number,输出类型为{a: number; b: number}
  • @jcalz 我打开了严格的空检查,这是导致问题的部分原因(尽管直到我刚刚关闭它才意识到这就是所涉及的)。如果我将其关闭,x 的类型将缩减为 number,一切都很好。
  • @jcalz 感谢您发现这个例子的问题,我试图做一个最小的例子,但不小心把它压缩得太多了。已编辑。
  • 我不建议关闭--strictNullChecks;它通常解决的问题多于产生的问题。

标签: typescript union-types object-object-mapping


【解决方案1】:

我想这取决于objectMap()的实现;假设对于Object.keys(target) 中的每个键k,在调用callback 之前,首先确保target[k] 不是undefined(如果k 是可选键,则可能发生这种情况)。如果是这样,那么您可以稍微修改objectMap() 的签名,以便callback 参数不需要担心undefined 参数,方法是使用predefined conditional type Exclude&lt;U, X&gt;... 接受联合类型@ 987654336@ 并删除任何可分配给X 的成分,如下所示:

function objectMap<I, O>(
  target: I,
  callback: (currentValue: Exclude<I[keyof I], undefined>) => O
) {
  const ret = {} as { [K in keyof I]: O };
  let k: keyof I;
  for (k in target) {
    const tk = target[k];
    if (typeof tk !== "undefined") {
      ret[k] = callback(tk as Exclude<I[keyof I], undefined>);
    }
  }
  return ret;
}

鉴于该签名,您现在可以像这样使用objectMap()

interface A {
  a?: number;
  b: number;
  c?: number;
}
const a: A = { a: 1, b: 2 };
const result = objectMap(a, x => x.toFixed());
// const result: {a?: string | undefined, b: string, c?: string | undefined }
console.log(result); // {a: "1", b: "2"}

所以在这种情况下,A 接口具有可选的数字属性 "a""c",以及必需的数字属性 "b"。回调的类型为(x: number)=&gt;string,返回的结果类型具有相同的可选和必需属性键,但值为string 类型。 (请注意,{a?: string | undefined, b: string, c?: string | undefined} 类型与 {a?: string, b: string, c?: string} 类型相同)如果编译器在编译时只知道 a 值是可以分配给 A 的,那么编译器可以做的最好的事情。

好的,希望对您有所帮助;祝你好运!

Link to code

【讨论】:

  • 实现是github.com/xixixao/object-map/blob/master/index.js(我正在尝试为现有库编写类型)。它不检查undefined,但我期待值存在但未定义的情况由类型系统处理。看起来#13195 准确地描述了我遇到的问题,现在无法精确输入。在此期间,我将编写自己的函数,使用Exclude 来处理这个问题。感谢您的帮助!