【问题标题】:Extract an object's properties and their generic types within a function's type annotation在函数的类型注释中提取对象的属性及其泛型类型
【发布时间】:2022-01-13 19:54:27
【问题描述】:

问题

如何在函数的类型注释中提取对象的属性及其泛型类型?

我需要能够推断出RequestData 的类型,以便该类型正确传播到request 回调。

目前在调用此函数时,我必须使用类型断言编写我的 request 回调。我希望我的 IDE 根据传入的 Form 对象推断 data 的类型。

此外,感谢任何关于如何在我的示例中摆脱 any 的建议。

谢谢!

示例

class Field<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }
}

class Form {
  // Object containing all Field objects in this Form
  fields: Record<string, Field<any>>;

  constructor({ fields }: { fields: Record<string, Field<any>>; ) {
    this.fields = fields;
  }
}



const submit = async <Response extends unknown, RequestData extends Record<string, unknown>>(
  form: Form,
  request: (data: RequestData) => Promise<Response>,
): Promise<Response> => {
  const fieldsAsGenerics = {} as RequestData; // Extract value stored in every form.field

  return await request(fieldsAsGenerics);
};



const myForm = new Form({
   fields: {
    name: new Field<string>('Charlie'),
    age: new Field<number | null>(66),
  }
});

submit(myForm, async (data) => {
  // data should be of type { [K in keyof typeof myForm.fields]: typeof myForm.fields[K] }
  // which in this case is { name: string; age: number | null }
  // Actual type is unknown
});

Playground Link

另见

【问题讨论】:

  • 对于 Typescript 问题,您可能需要考虑链接到 Typescript playground。回答者运行您的代码变得更快:)
  • 谢谢@Mack!我刚刚添加了游乐场链接

标签: typescript typescript-generics


【解决方案1】:

这个问题的根源是您的Form 类型不是通用的,因此编译器不知道有关存在的字段类型的任何信息。一个Form 对象在编译时无法与任何其他Form 区分开来,因此它们不能根据它们所持有的字段类型而具有不同的行为。

所以,让我们将 Form 设为它所包含的包含 Record 的字段的类型。

class Form<T extends Record<string, Field<any>>> {
  // Object containing all Field objects in this Form
  fields: T

  constructor({ fields }: { fields: T}) {
    this.fields = fields;
  }
}

现在,我们使用实用程序类型从上述记录类型中提取 RequestData 类型(实际上是移除 Field 包装器)。

type ExtractFieldValues<T extends Record<string, Field<any>>> = {
    [K in keyof T]: T[K] extends Field<infer V> ? V : never;
};

我们可以调整submit 函数以使用新类型,但需要注意一点。当您从Field 的实例中拆箱值时,总是需要一个类型断言。编译器不够警觉,无法分析您可能使用的任何循环并保证已包含 Fields 类型的每个属性:

const submit = async <Response extends unknown, Fields extends Record<string, Field<any>>>(
  form: Form<Fields>,
  request: (data: ExtractFieldValues<Fields>) => Promise<Response>,
): Promise<Response> => {
  const fieldsAsGenerics = {
      // Extract value stored in every form.field
  } as ExtractFieldValues<Fields>;
  // A type assertion will ALWAYS be required here:
  // the compiler can't infer that you're looping over every
  // property of the form type and extracting T from Field<T>.
  
  return await request(fieldsAsGenerics);
};

为避免类型断言,将值直接存储在 fields 对象中而不使用 Field 包装器可能是明智之举,但根据您的代码的其余部分,这可能是不可能的或有用的。

现在可以在使用中正确推断类型(将鼠标悬停在 Playground 链接中进行确认):

const myForm = new Form({
 fields: {
    name: new Field<string>(/*Field constructor argsargs*/),
    age: new Field<number | null>(/*Field constructor args*/)
  }
});

submit(myForm, async (data) => {
  // data should be of type { [K in keyof typeof myForm.fields]: typeof myForm.fields[K] }
  // which in this case is { name: string; age: number | null }
});

Playground link

【讨论】:

  • 你是救生员 :D 这非常有效。您说得对,在我的情况下我无法删除 Field 包装器,因为该类不仅仅包含值。
  • 我不介意保留类型断言as ExtractFieldValues&lt;Fields&gt;;。这远比必须在data 对象上声明每个属性的类型更令人沮丧。
猜你喜欢
  • 2018-07-23
  • 2019-09-29
  • 2020-03-23
  • 1970-01-01
  • 2023-03-28
  • 2021-01-30
  • 1970-01-01
  • 1970-01-01
  • 2020-01-29
相关资源
最近更新 更多