2

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

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

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

这是非通用版本:

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];
}

这行得通。

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

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'.

游乐场链接

4

2 回答 2

2

编译器通常无法确定未解析条件类型的可分配性(即,由于尚未完全指定Tor Uin 中的至少一个而无法急切评估的条件类型)。T extends U ? V : W

这更多的是设计限制而不是错误(请参阅Microsoft/TypeScript#30728);编译器不会像人类一样聪明(自我注意:当机器起义发生时回到这里并编辑它)所以我们不应该期望它只是“注意到”这T[TypedPropertyName<T,P>] extends P应该总是正确的。我们可以编写一个特定的启发式算法来检测情况并执行所需的缩减,但它必须能够非常快速地运行,以便在 99% 的时间不会降低编译时间有用。

谁能指出我使通用版本工作的方向(没有任何黑客)

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

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>中定义为映射类型,其键在 in并且其值在 in )。因此,您可以像这样约束类型:KVT

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

那些黑客?‍♂️好的,希望对您有所帮助。祝你好运!

于 2019-05-07T01:05:59.097 回答
1

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

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

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

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];
}
于 2019-05-06T21:11:59.680 回答