网站搜索

for-in 循环将 TypeScript 类型转换为交叉类型?


编辑:更新后的最小示例(希望如此):

归结起来,我想我好奇的核心是为什么这行不通,并且会导致下面的两个错误:

interface Implicit {
  someNumber?: number | { value: number }
  someString?: string | { value: string }
}

interface Explicit {
  someNumber?: { value: number }
  someString?: { value: string }
}

const impl:Implicit = {
  someNumber: 123,
  someString: { value: 'impl' }
}

function convert (obj: Implicit) {
  const expl:Explicit = {}
  let k: keyof typeof obj;
  for (k in obj) {
    const objk = obj[k] // also, why does this alter behavior?
    if (typeof objk === 'object') {
      expl[k] = objk // Error {value:string}|{value:number} not assignable to {value:string}&{value:number}
    } else {
      expl[k] = { value: objk } // Error string|number is not assignable to never
    }
  }
}

TS不能在每次循环中确定k是someNumber还是someString,所以它创建了一个交集,强制结果同时是两者吗?

更新的 TS 游乐场


上一个问题:

我正在尝试创建一个通用函数,该函数可以接受简写的隐式表示法 { someParameter: 123 } 或显式版本:{ someParameter: { value: 123, min: 0, max: 200 }},并转换后返回显式版本:

interface ImplicitValues {
  color: number | string | ExplicitValueColor;
  position: number | ExplicitValuePosition;
}

interface ExplicitOptions {
  min?: number
  max?: number
}

interface ExplicitValueColor extends ExplicitOptions {
  value: number | string
}
interface ExplicitValuePosition extends ExplicitOptions{
  value: number,
}

还有其他隐式值类型(为了简洁未列出),所以我想创建一个通用类型,能够自动转换为显式类型:

type MakeExplicit<T> = {
    [K in keyof T]?: Extract<T[K], { value: any }>
}

这似乎有效,因为当我们使用鼠标悬停在使用 @jcalz 出色的 Expand 工具的类型上时,它是正确的:

然而,当我们尝试遍历 ImplicitValues 数组,并将它们显式化时,我们会得到以下错误:

const someObject:ImplicitValues = {
  color: 0xff0000,
  position: {
    value: 123,
    min: 0,
    max: 200
  }
}

function isExplicit <T>(param: any): param is MakeExplicit<T> {
  if (typeof param === 'object') return 'value' in param;
  return false;
}

function convertToExplicit(obj:ImplicitValues) {
  let explicitValues:MakeExplicit<ImplicitValues> = {};

  let key:keyof typeof obj
  for (key in obj) {
    if (isExplicit(obj[key])) {
      explicitValues[key] = obj[key]
      // ERROR: Type 'string | number | ExplicitValueColor | ExplicitValuePosition' is
      // not assignable to type 'ExplicitValueColor & ExplicitValuePosition'.
    } else {
      explicitValues[key] = { value: obj[key] }
      // ERROR: Type 'string | number | ExplicitValueColor | ExplicitValuePosition' is
      // not assignable to type 'number'.
    }
  }
  return explicitValues
}

手动设置这些值可以正常工作,但在循环中动态设置就不行。我是应该直接强制转换 explicitValues[key]=(obj as any)[key] 然后了事,还是有更好的方法来做这件事?

如果有帮助的话,链接到 TS Playground。

答案#1

这里的主要问题是,由于 k 是联合类型,因此编译器也将 obj[k] 看作是联合类型:

const objk = obj[k];
/*  string | number | { value: number; } | { value: string; } */

即使你从中消除了原始类型,obj[k] 仍然是联合类型:

if (typeof objk === "object") {
  objk; /* { value: number } | { value: string } */
}

所以编译器所知道的只是你有一个类型为 {value: number} | {value: string} 的值 objk 和一个类型为 keyof Implicit 的键 k,而你正试图赋值 expl[k]=objk,其中 expl 的类型为 Explicit。通常来说,这是不安全的。就编译器所看到的情况而言,k 有可能是 "someNumber",而此时 objk 的类型却是 {value: string}:

expl[k] = objk; // error!
// Type '{ value: number; }' is not assignable to type '{ value: string; }'

(如果你想知道为什么错误消息涉及交叉类型,请参见 microsoft/TypeScript#30769。如果你有一个类型为 T 的对象 t 和一个类型为 K1 | K2 的(非泛型)键 k,并且你想给 t[k] 赋值,那么唯一安全的赋值就是适用于 T[K1] 和 T[K2] 的值,即 T[K1] & T[K2]。当你读取 t[k] 时,你得到的是 T[K1] | T[K2],但是当你写入 t[k] 时,你需要 T[K1] & T[K2]。)

我们知道这是不可能的,因为我们知道 k 和 objk 是相关的,但类型系统无法追踪这一点。因此会出现错误。对于相关联合类型的缺乏支持,是 microsoft/TypeScript#30581 讨论的主题。


处理这个问题最简单的方法就是使用类型断言。当你比编译器更了解某个表达式的类型时,你可以直接告诉编译器这个信息。或者你也可以直接告诉编译器不要担心,使用 any 断言:

expl[k] = obj[k] as any; // I know what I'm doing

没问题,但这会将部分类型安全的责任从编译器转移到你自己身上。如果你写错了东西,编译器是不会发现的:

expl[k] = obj["someNumber"] as any; // oops

如果你想从编译器获得更多类型安全保证,你需要使用泛型而不是联合类型,特别是你需要将你的 Explicit 和 Implicit 类型重构为泛型,并依赖一个公共接口,这样赋值 expl[k]=objk 就会被视为一个赋值,其中两边在该接口和泛型键 K 的意义下是相同的。

该方法在 microsoft/TypeScript#47109 中进行了描述,这是对 microsoft/TypeScript#30581 的修复(有趣的是,这里的重构在没有该拉取请求中的更改的情况下也能工作,因为大多数机制已经是语言的一部分)。

如果你有一个类型为 T 的对象 t,一个 泛型 键类型为 K 的键 k,以及一个类型为 T[K](也将是泛型)的值 v,那么编译器 会 允许你写 t[k]=v,即使如果 K 是非泛型联合类型同样的赋值会失败。(由于相同的原因,这实际上是不安全的,但编译器允许它。只要我们小心不要将 K 指定为联合类型,就没有问题。)

它看起来是这样的:

interface Base {
  someNumber: number;
  someString: string;
}

type Explicit<K extends keyof Base = keyof Base> =
  { [P in K]?: { value: Base[P] } };

type Implicit<K extends keyof Base = keyof Base> =
  { [P in K]: Base[P] | { value: Base[P] } };

Explicit 和 Implicit 类型在没有类型参数时的评估结果与原始版本相同,但现在你可以为某个特定的键写 Explicit<K>,编译器将知道它在该键上具有相应形状的(可选)属性。

现在让我们编写一个通用的assign()函数,它执行你在for循环中所做的赋值操作:

function assign<K extends keyof Base>(
  k: K,
  expl: Explicit<K>,
  objk: Base[K] | { value: Base[K] }
) {
  if (typeof objk === 'object') {
    expl[k] = objk  // okay
  } else {
    expl[k] = { value: objk }  // okay
  }
}

这可以在没有错误的情况下编译,因为在这两种情况下,你都将类型为 {value: Base[K]} 的值分配给相同泛型类型的属性。你可以看到,你获得的类型安全比类型断言更多,因为编译器会对将随机其他内容分配给 expl[k] 提出警告:

const obj: Explicit = null!    
expl[k] = obj; // error!

一旦你有了 assign(),你就可以在 convert() 中调用它:

function convert(obj: Implicit) {
  const expl: Explicit = {};
  let k: keyof typeof obj;
  for (k in obj) {
    assign(k, expl, obj[k]); // okay
  }
}

请注意,你并不需要声明一个单独的 assign() 函数,但你确实需要在某处有一个通用函数。它可以是 forEach() 的回调,或者一个内联函数:

function convert(obj: Implicit) {
  const expl: Explicit = {};
  let k: keyof typeof obj;
  for (k in obj) {
    (<K extends keyof Base>(
      k: K, expl: Explicit<K>, objk: Base[K] | { value: Base[K] }
    ) => expl[k] = (typeof objk === 'object') ? objk : { value: objk })(
      k, expl, obj[k]
    );
  }
}

就是这样!这个重构可能不值得麻烦;如果你足够小心,类型断言也是可以的。

代码的 Playground 链接