TypeScript:如何在循环中正确使用生成的联合类型?

TypeScript: How to use generated Union Type correctly in a loop?

我有以下类型:

type Updater<T> = {
  [K in keyof T]: {
    key: K;
    update: (value: string) => T[K];
  };
}[keyof T]

这里我的意图是为接受输入的任何类型提供一些通用更新程序,并使用更新函数更新每个个体 属性,该更新函数具有强制类型,因此输出与 [= 的类型相匹配40=]。达到此目的:

type TestType = {
  fieldA: string;
  fieldB: number;
}

type TestTypeUpdater = Updater<TestType>[];

const testTypeUpdater: TestTypeUpdater = [
  {
    key: "fieldA", // enforced to be one of "fieldA" or "fieldB"
    update: s => s + "(updated)" // enforced to output the type of "fieldA"
  },{
    key: "fieldB",
    update: s => s.length      // enforced to output the type of "fieldB"
  }
];

但是,当我尝试使用它时,它并不像我预期的那样。我想做的是:

const testType: TestType = {
  fieldA: "",
  fieldB: 0
}

function test(input: string) {
  for (const u of testTypeUpdater) {
    testType[u.key] = u.update(input);
  }
}

test("input");

我原以为它和上面一样简单,但 TypeScript 在 testType[u.key]:

下抱怨
Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.ts(2322)

似乎 update 的 return 类型是 T[K] 的事实丢失了,但变成了 string | number 的并集。我必须做的是:

function testOK(input: string) {
  for (const u of testTypeUpdater) {
    if (u.key === "fieldA") {
      testType[u.key] = u.update(input);
    } else if (u.key === "fieldB") {
      testType[u.key] = u.update(input);
    }
  }
}

testOK("input");

事实上,我需要在循环中的类型中再次重复每个 属性 名称,这破坏了使用 keyof 生成联合类型的整个目的。

如何在无需检查 属性 名称的情况下使循环版本有效?


添加: 如果 Updater 是一个字典类型,例如:

type Updater<T> = {
  [K in keyof T]: {
    update: (value: string) => T[K];
  };
}

type TestType = {
  fieldA: string;
  fieldB: number;
}

type TestTypeUpdater = Updater<TestType>;

const testTypeUpdater: TestTypeUpdater = {
  fieldA: { // enforced to be one of "fieldA" or "fieldB"
    update: s => s + "(updated)" // enforced to output the type of "fieldA"
  },
  fieldB: {
    update: s => s.length      // enforced to output the type of "fieldB"
  }
};

const testType: TestType = {
  fieldA: "",
  fieldB: 0
}

function test(input: string) {
  Object.keys(testTypeUpdater).forEach(k => {
    testType[k] = testTypeUpdater[k].update(input);
  });
}

test("input");

你 运行 遇到了一个我称之为“相关联合类型”的问题,正如 microsoft/TypeScript#30581. Many cases of this have been addressed by indexed access inference improvements as implemented in microsoft/TypeScript#47109 中所讨论的,尽管它往往需要一些重构。


的问题
function test(input: string) {
  for (const u of testTypeUpdater) {
    testType[u.key] = u.update(input);
  }
}

是编译器无法跟踪类型u.keyu.update(input)类型之间的相关性。这是它看到的:

const uKey = u.key; // "fieldA" | "fieldB"
const uUpdated = u.update(input) // string | number
testType[uKey] = uUpdated; // error!

请注意 uKeyuUpdated 分别属于 union types。这些类型没有错误uKey 确实可以是这两个字符串文字中的任何一个,而 uUpdated 可以是 stringnumber。但它将这些视为 independentuncorrelated 类型。就编译器可以从这些类型来看,uKey 可能是 "fieldA"uUpdated 可能是 number。因此分配失败。

无法跟踪联合相关性是 microsoft/TypeScript#30581 的主题。您可以通过编写冗余代码来解决它,如您所示...或使用 type assertions 来抑制错误:

testType[uKey] = uUpdated as never; // fixed I guess?

但是...您也可以使用 generics and indexed access types 进行重构。基本思想是使用 分布式对象类型 ,如 microsoft/TypeScript#47109 中创造的那样。以下是我们可以对 Updater<T>:

执行的操作
type Updater<T, K extends keyof T = keyof T> = { [P in K]: {
    key: P;
    update: (value: string) => T[P];
}; }[K]

所以类型 Updater<T> 与以前相同,但是您可以通过将其指定为 Updater<T, K> 来将其缩小到 T 的一个键 K。例如:

type TestTypeUpdaterEither = Updater<TestType>;
/* type TestTypeUpdaterEither = {
    key: "fieldA";
    update: (value: string) => string;
} | {
    key: "fieldB";
    update: (value: string) => number;
} */

type TestTypeUpdaterFieldA = Updater<TestType, "fieldA">
/* type TestTypeUpdaterFieldA = {
    key: "fieldA";
    update: (value: string) => string;
} */

现在,我们将您的 for..of 循环更改为 forEach() 调用,因此我们可以传入 K 中通用的回调,[=37] 的一些特定键=].编译器允许将 TestType[K] 分配给 TestType 值的通用 K 属性。 (这实际上也是不安全的,如果 K 是用联合指定的,但编译器允许这样做,我们需要使用它才能继续。我们实际上不会用它做任何不安全的事情。)

看起来像这样:

testTypeUpdater.forEach(<K extends keyof TestType>(u: Updater<TestType, K>) => {
    const uKey = u.key; // K
    const uUpdated = u.update(input) // TestType[K]
    testType[uKey] = uUpdated; // okay
}

有效!哦,我想我们不需要将它们分解成单独的变量:

testTypeUpdater.forEach(<K extends keyof TestType>(u: Updater<TestType, K>) => {
    testType[u.key] = u.update(input); // okay
})

仍然有效。

Playground link to code