For-in 循环将 TypeScript 类型转换为交集?

For-in loop converting TypeScript types to intersection?

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

归根结底,我想我好奇的核心是为什么这行不通,并导致以下两个错误:

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每次循环都无法判断ksomeNumber还是someString,所以是在创建交集 将结果强制为两者?

Updated TS playground


上一题:

我正在尝试创建一个多功能函数,它接受 shorthand、隐式表示法 { someParameter: 123 } 或显式版本:{ someParameter: { value: 123, min: 0, max: 200 }},并转换和 returns显式版本:

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,
}

还有其他 ImplicitValue 类型(为简洁起见未列出),所以我想创建一个通用类型,即 自动转换为显式类型:

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

这似乎可行,因为当我们使用@jcalz awesome 实用程序将鼠标悬停在类型上时,它是正确的:

但是,当我们尝试遍历 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] 然后结束它,还是有更好的方法来做到这一点?

Link to TS playground,如果有帮助的话。

这里的主要问题是,由于 k 属于 union type,因此编译器也将 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 其中 explExplicit 类型。一般来说,那是不安全的。就编译器而言,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],那么分配的唯一安全的东西就是适用于 both 的东西T[K1] T[K2],或T[K1] & T[K2]。当你阅读 t[k]时,你会得到T[K1] | T[K2],但是当你 t[k]时你需要T[K1] & T[K2]。)

我们知道这是不可能的,因为我们知道kobjk都是相关的,但是类型系统无法追踪。因此错误。 相关联合类型 的不支持是 microsoft/TypeScript#30581.

的主题

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

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

这很好,但是它将类型安全的一些责任从编译器上移开并交给了您。如果你写错了,编译器不会捕捉到它:

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

如果你想从编译器那里获得更多的类型安全保证,你将需要使用 generics 而不是联合,特别是你需要重构你的 ExplicitImplicit 类型是通用的并依赖于一个公共接口,因此赋值 expl[k] = objk 被视为一个赋值,其中双方在该接口和通用键 K 方面是相同的。

该方法在 microsoft/TypeScript#47109 中进行了描述,这是对 microsoft/TypeScript#30581 的修复(有趣的是,此处的重构无需更改该拉取请求即可工作,因为大多数机制已经成为语言)。

如果您有一个 t 类型 T 的对象,一个 generic 键类型 K 的键 k,和类型 T[K] 的值 v(也将是通用的),那么编译器 允许您编写 t[k] = v,即使如果 K 是 non-generic 并集,则相同的赋值会失败。 (出于同样的原因,这证明是不安全的,但编译器允许它。只要我们注意不要用 union-type 指定 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] } };

没有类型参数的 ExplicitImplicit 类型的计算结果与您的原始版本相同,但现在您可以为某些特定键编写 Explicit<K> 并且编译器会知道它在相应形状的那个键上有一个(n 可选)属性。

现在让我们编写一个通用的 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 link to code