TypeScript 中的通用嵌套值 Getter

Generic Nested Value Getter in TypeScript

我正在尝试编写一个从给定对象获取嵌套值的 TS 函数。该对象可以是多种类型之一,因此我使用的是泛型。但是,TS 抱怨所以我觉得我误解了泛型在 TS 中的工作方式:

interface BaseValueObject<T> {
  value: T | null
}

type StringValue = BaseValueObject<string>
type NumberValue = BaseValueObject<number>

interface FormAData {
  name: StringValue,
  age: NumberValue
}

interface FormBData {
  height: NumberValue
  nickname: StringValue
}

interface FormA {
  data: FormAData
}

interface FormB {
  data: FormBData
}

type Form = FormA | FormB

const getFormValue =
  <F extends Form, P extends keyof F['data']>(form: F, property: P) =>
    form['data'][property]['value'] // Type 'P' cannot be used to index type 'FormAData | FormBData'

所需用法:

const formARecord: FormA = {
  data: {
    name: {
      value: 'Joe'
    },
    age: {
      value: 50
    }
  }
}
const joesAge = getFormValue(formARecord, 'age')
console.log(joesAge) // 50

Playground

解决方案

这是我最后做的,类似于@jered 在他的回答中建议的:

playground

基本上,该课程是明确您输入的任何不变量。就我而言,我并没有正式告诉编译器 every 属性 FormADataFormBData 遵循相同的接口。我能够通过从这个基本接口扩展它们来做到这一点:

...
type Value = StringValue | NumberValue

interface BaseFormData {
  [property: string]: Value
}

interface FormAData extends BaseFormData {
  ...

您应该将泛型声明扩展到“表单”接口本身。

在这种情况下,您需要为 TypeScript 提供一种方法来“推断”formdata 属性 的类型,以便 property 正确索引它。

您目前的编写方式会出错,因为您不能使用 keyof 来提取联合类型的属性。考虑这个例子:

type Foo = {
  fooProperty: string;
}
type Bar = {
  barProperty: string;
}
type Baz = Foo | Bar;
type Qux = keyof Baz; // type Qux = never

Qux 应该是什么类型?它不能同时是两种不同类型的键,所以它最终是 never 类型,这对于索引对象的属性不是很有用。

相反,请考虑您的基础 Form 类型本身是否是泛型,您知道应该始终有一个 data 属性,但是那个 [=15] 的特定类型=] 属性 在实际使用之前是未知的。幸运的是,您仍然可以限制 data 的某些方面,以确保在您的应用程序中强制执行其结构:

interface FormDataType {
  [key: string]: StringValue | NumberValue;
};

interface Form<T extends FormDataType> {
  data: T
};

然后当你编写你的 Form 的风格时,它对 data 属性 有更具体的类型定义,你可以这样做:

type FormA = Form<{
  name: StringValue,
  age: NumberValue
}>;

type FormB = Form<{
  height: NumberValue
  nickname: StringValue
}>;

在某种程度上,这有点像“扩展”类型,但在某种程度上允许 TypeScript 使用 inferForm 泛型(字面意思)稍后 data 的类型。

现在我们可以重写 getFormValue() 函数的泛型类型以匹配函数签名中的 Form 泛型。理想情况下,函数的 return 类型可以从函数参数和函数体中完美地推断出来,但在这种情况下,我没有找到一种好的方法来构造泛型,以便无缝地推断出一切。相反,我们可以直接转换函数的 return 类型。这样做的好处是 1. 仍然检查 form["data"] 是否存在并匹配我们之前建立的 FormDataType 结构,以及 2. 推断值的 actual 类型 return调用 getFormValue(),提高您的整体类型检查信心。

const getFormValue = <
  F extends Form<FormDataType>,
  P extends keyof F["data"]
>(
  form: F,
  property: P
) => {
  return form["data"][property].value as F["data"][P]["value"];
}

Playground

编辑:进一步思考 Form 接口本身的泛型并不是真正必要的,你可以做一些其他的事情,比如声明一个基本的 Form 接口,然后 extend 它每个具体形式:

interface FormDataType {
  [key: string]: StringValue | NumberValue;
}

interface Form {
  data: FormDataType
};

interface FormA extends Form {
  data: {
    name: StringValue;
    age: NumberValue;
  }
};

interface FormB extends Form {
  data: {
    height: NumberValue;
    nickname: StringValue;
  }
};

const getFormValue = <
  F extends Form,
  P extends keyof F["data"]
>(
  form: F,
  property: P
) => {
    return form["data"][property].value as F["data"][P]["value"];
}