在下面的脚本代码中,我根据动态nameFieldidField参数计算idname,这是一个复杂函数的最小示例,我如何将其转换为类型脚本并要求TS确保option具有nameFieldidField字段?

namestring,idnumber

export const foo =
  (
    option,
    nameField = 'name',
    idField = 'id',
  ) => {
    const name = option[nameField];
    const id = option[idField];
    const result =  { id, name };
    /// ... other codes
  }

我尽了最大努力

export const foo = <T extends Record<string, any>>(
  option: T,
  nameField: keyof T = 'name' as keyof T,
  idField: keyof T = 'id' as keyof T,
) => {
  const name = option[nameField];
  const id = option[idField];
  const result =  { id, name };
  /// ... other codes
};

我预计以下代码中会出现错误

const option = {
  id: '1', // Should be number
  name: 'Foo'
  value: 2
};

const a = foo(option);  // Error here
const b = foo(option,'name','value');  // OK

推荐答案

您可以采取的一种方法是将函数generic设置为类型NnameField和类型IidField,然后使option的类型已知在键N处具有string属性,在键I处具有number属性.这意味着调用签名就像

const foo = <N extends PropertyKey, I extends PropertyKey>(
  option: Record<N, string> & Record<I, number>,
  nameField: N, 
  idField: I
) => {};

Record<K, V> utility type指的是在类型K的键处具有类型V的属性的对象,而intersection组合了这两种类型.因此Record<N, string> & Record<I, number>意味着"在关键字N处具有string属性并且在关键字I处具有number属性的类型".


好的,但是如果调用方没有提供参数,您希望nameFieldidField分别为optional,缺省值分别为"name""id".这不适合泛型,因为在这种情况下,无法知道NI(由调用者 Select )实际上是"name"还是"id"(由实现 Select ).请参见How to give a default value to a generic parameter?microsoft/TypeScript#49158.在这两个地方提到的一个解决办法是,如果没有提供nameFieldidField的参数,则给出NI default type arguments,然后只给assert "name""id"NI类型的.这看起来像是:

const foo = <
  N extends PropertyKey = "name", 
  I extends PropertyKey = "id"
>(
  option: Record<N, string> & Record<I, number>,
  nameField = 'name' as N, 
  idField = 'id' as I
) => {};

这接近于我们想要的结果,除了一些反复无常的泛型类型推断将导致从option推断出NI,如果nameFieldidField被省略.但我们真的不想要这种行为;NI应该从nameFieldidField推断,或者说默认为"name""id"……而option应该只是checked.这意味着Record<N, string> & Record<I, number>内的NI应该是microsoft/TypeScript#14829中所要求的non-inferential type parameter usages.这里没有本机NoInfer可用,所以我们不能在自己不实现NoInfer的情况下编写Record<NoInfer<N>, string> & Record<NoInfer<I>, number>.GitHub问题中提到的一种技术是与空对象类型(如type NoInfer<T> = T & {})相交,这会降低推理优先级.让我们在这里这样做:

const foo = <
  N extends PropertyKey = 'name',
  I extends PropertyKey = 'id'
>(
  option: Record<N & {}, string> & Record<I & {}, number>,
  nameField = 'name' as N,
  idField = 'id' as I,
) => {
  const name: string = option[nameField];
  const id: number = option[idField];
  const result = { id, name };
  console.log(id.toFixed())
};

这是我们的最终形式.


让我们来测试一下:

const option = {
  id: '2',
  name: 'Foo',
  value: 2
};

const a = foo(option);  // Error here
const b = foo(option, 'name', 'value');  // okay

const c = foo({ name: "abc", id: 123 }); // okay
const d = foo({ name: "abc", id: "123" }); // error

一切都如你所愿.请注意,泛型缺省值的解决方法确实存在潜在的失败模式,在这种模式下,调用方手动指定NI类型参数,而不传入nameFieldidField参数:

foo<"name", "value">(option); // no compiler error!

这不太可能,但你应该意识到这一点.

Playground link to code

Typescript相关问答推荐

返回对象的TypScript构造函数隐式地替换This的值来替换super(.)的任何调用者?

对于使用另一个对象键和值类型的对象使用TS Generics

如何在方法中定义TypeScript返回类型,以根据参数化类型推断变量的存在?

TypeScript实用程序类型—至少需要一个属性的接口,某些指定属性除外

数组文字中对象的从键开始枚举

使用泛型keyof索引类型以在if-condition内类型推断

如何使用泛型自动推断TS中的类型

向NextAuth会话添加除名称、图像和ID之外的更多详细信息

使用打字Angular 中的通用数据创建状态管理

类型脚本返回的类型包含无意义的泛型类型名称

为什么有条件渲染会导致material 自动完成中出现奇怪的动画?

尽管对象的类型已声明,但未解析的变量<;变量

Angular 自定义验证控件未获得表单值

返回嵌套props 的可变元组

为什么Typescript只在调用方提供显式泛型类型参数时推断此函数的返回类型?

基于泛型的条件接口键

如何在Reaction 18、Reaction-Rout6中的导航栏中获取路由参数

TypeScript中的这些条件类型映射方法之间有什么区别?

从泛型对象数组构造映射类型

Typescript 是否可以区分泛型参数 void?