I'm in the process of applying better TypeScript to a project. There is a complex object coming from my backend which is a class with dozens of attributes, most of which are classes of their own. We have a form with a common change handler that currently has a few anys in it to make it work, but I'd like to fix that with generics.

This is a simplified version of the pattern in my application, but I am able to reproduce the same errors I'm seeing:

class PhoneNumber {
  prefix: number;
  exchange: number;
  line: number;
}

class Address {
  address1: string;
  address2: string;
  city: string;
}

class User {
  phone: PhoneNumber;
  address: Address;
}

const user = new User();

function setValue<A extends keyof User, T extends User[A], K extends keyof T, V extends T[K]>(
  value: V,
  fieldType: new () => T,
  fieldName: A,
  fieldAttribute: K
) {

  if (!user[fieldName]) {
    user[fieldName] = new fieldType();
  }

  const field = user[fieldName];

  // error here.
  field[fieldAttribute] = value;
}

setValue(408, PhoneNumber, 'phone', 'prefix');

I tried a few things, and this is as close as I'm able to get to a solution. I'm able to inspect the function call at the end in my IDE, and it looks like the generics are being filled with something sane: function setValue<"phone", PhoneNumber, "prefix", number>(), but then I get an error during compilation:

TS2536: Type 'K' cannot be used to index type 'User[A]'.

Am I going about this the wrong way? If I can't figure this out, I'm going to end up just splitting out the different types into separate handlers.

There's also a case where some of the fields are arrays of objects, which adds another wrinkle to the whole thing.

推荐答案

With the current argument types of setValue, one could provide a child class B as fieldType (which extends user[fieldName] class A), and target a fieldAttribute of B, which may not exist in A:

class PhoneNumber2 extends PhoneNumber {
  extension: number;
}

// Complies with setValue types,
// but if `user` already has a basic PhoneNumber instance,
// the code within setValue fails
setValue(408, PhoneNumber2, "phone", "extension");

// within setValue:
user["phone"]["extension"] = 408;
// ...but user["phone"] may be a PhoneNumber,
// not necessarily a PhoneNumber2!

In your case, instead of K extends keyof T (T being the return type of fieldType, potentially a child class), you could just use K extends keyof User[A], to make sure the fieldAttribute targets the base class:

function setValue2<A extends keyof User, T extends User[A], K extends keyof User[A], V extends T[K]>(
  value: V,
  fieldType: new () => T,
  fieldName: A,
  fieldAttribute: K
) {

  if (!user[fieldName]) {
    user[fieldName] = new fieldType();
  }

  const field = user[fieldName];

  field[fieldAttribute] = value; // Now okay
}

setValue2(408, PhoneNumber, 'phone', 'prefix');
setValue2(408, PhoneNumber2, 'phone', 'prefix');
setValue2(408, PhoneNumber2, 'phone', 'extension'); // Error: Argument of type '"extension"' is not assignable to parameter of type 'keyof PhoneNumber'.

You could even get rid of the generic type T:

function setValue3<A extends keyof User, K extends keyof User[A], V extends User[A][K]>(
    value: V,
    fieldType: new () => User[A],
    fieldName: A,
    fieldAttribute: K
) {}

Playground Link

Typescript相关问答推荐

TypScript在对象上强制执行值类型时推断对象文字类型?

APP_INITIALIZER—TypeError:appInits不是函数

React router 6.22.3不只是在生产模式下显示,为什么?

为什么tsx不判断名称中有"—"的属性?'

TypeScrip-将<;A、B、C和>之一转换为联合A|B|C

为什么不能使用$EVENT接收字符串数组?

在Reaction-Native-ReAnimated中不存在Animated.Value

如何避免推断空数组

推断从其他类型派生的类型

为什么我的导航在使用Typescript的React Native中不起作用?

我在Angular 17中的environment.ts文件中得到了一个属性端点错误

跟踪深度路径时按条件提取嵌套类型

在类型脚本中将泛型字符串数组转换为字符串字母并集

RXJS将对键盘/鼠标点击组合做出react

类型脚本-在调用期间解析函数参数类型

如何处理可能为空的变量...我知道这不是Null?

如何知道使用TypeScript时是否在临时工作流内运行

对于通过 Axios 获取的数据数组,属性map在类型never上不存在

从 npm 切换到 pnpm 后出现Typescript 错误

Typescript 编译错误:TS2339:类型string上不存在属性props