将分布式条件类型与泛型方法结合使用时出现问题

Issue when using distributive conditional types combined with generic method

我一直在尝试创建一个通用函数,它接收和对象 T 并接收该对象 T 的字符串 属性 名称。

我以 https://www.typescriptlang.org/docs/handbook/advanced-types.html 为例(部分:分布式条件类型)

我想出了一个没有泛型的解决方案,但是当我将显式类型更改为泛型类型时,typescript 将无法编译。

这是非通用版本:

export type TypedPropertyNames<T, P> = { [K in keyof T]: T[K] extends P ? K : never }[keyof T];
export type StringPropertyNames<T> = TypedPropertyNames<T, string>;

interface Test {
  test: string;
}
 
function non_generic(form: Test, field: StringPropertyNames<Test>): string {
  return form[field];
}

这有效。

现在,当我将测试接口更改为通用参数时,它将不再编译。

export type TypedPropertyNames<T, P> = { [K in keyof T]: T[K] extends P ? K : never }[keyof T];
export type StringPropertyNames<T> = TypedPropertyNames<T, string>;

function generic<T>(form: T, field: StringPropertyNames<T>): string {
  return form[field]; // This won't compile
}

这是预期的行为吗?或者这是一个打字稿错误? 谁能指出我使通用版本工作的方向(没有任何黑客)

更新 1:

编译错误:

Type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]' is not assignable to type 'string'.

Playground link

老实说,我不知道问题出在哪里。您可以尝试在他们的 GH 上提交问题。但是,我知道以下内容在没有明确指定 return 类型的情况下确实有效:

function generic<T>(form: T, field: StringPropertyNames<T>) {
  return form[field];
}

它甚至可以将 return 值正确地键入为字符串:

const test = {
  a: "b",
  c: 1,
  "other": "blah"
}
generic(test, "a").charAt(0) //passes - "b"
generic(test, "a") * 5 // fails - function result is not a number
generic(test, "c") //fails - "c" is not assignable to "a" | "other"

我还建议添加此内容以确保第一个参数必须是一个对象:

function generic<T extends object>(form: T, field: StringPropertyNames<T>) {
  return form[field];
}

编译器通常无法确定未解决的条件类型的可赋值性(即无法急切求值的条件类型,因为 T extends U ? V : W 中的 TU 中至少有一个是尚未完全指定)。

这与其说是错误,不如说是设计限制(参见 Microsoft/TypeScript#30728);编译器不会像人类一样聪明(自我提醒:当机器起义发生时回到这里并编辑它)所以我们不应该期望它只是 "notice" T[TypedPropertyName<T,P>] extends P应该永远是真的。我们可以编写一个特定的启发式算法来检测情况并执行所需的减少,但它必须能够 运行 非常快 这样它就不会降低编译99% 的时间都没有用。

Can anyone point me in the direction of making the generic version work (without any hacks)

这真的取决于你认为什么是 hack。绝对最简单的做法是使用 type assertion,它明确用于当您知道某些东西是类型安全但编译器无法弄清楚它的时候:

function generic<T>(form: T, field: StringPropertyNames<T>): string {
  return form[field] as any as string;  // I'm smarter than the compiler 
}

或者您可以尝试通过必要的步骤引导编译器了解您正在做的事情是安全的。特别是,编译器 确实 理解 Record<K, V>[K] 可分配给 V(其中 Record<K, V>the standard library 中定义为映射类型其键在 K 中,其值在 V 中)。因此,您可以像这样限制类型 T

function generic<T extends Record<StringPropertyNames<T>, string>>(
  form: T,
  field: StringPropertyNames<T>
): string {
  return form[field]; // okay
}

现在编译器很高兴。并且约束 T extends Record<StringPropertyNames<T>, string> 根本不是一个真正的约束,因为任何对象类型都会符合它(例如,{a: string, b: number} extends Record<'a', string>)。所以你应该能够在任何你使用原始定义的地方使用它(无论如何对于具体类型T):

interface Foo {
  a: string;
  b: number;
  c: boolean;
  d: "d";
}
declare const foo: Foo;
generic(foo, "a"); // okay
generic(foo, "d"); // okay

这些是黑客攻击吗? ‍♂️ 好的,希望对您有所帮助。祝你好运!