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
解决方案
这是我最后做的,类似于@jered 在他的回答中建议的:
基本上,该课程是明确您输入的任何不变量。就我而言,我并没有正式告诉编译器 every 属性 FormAData
和 FormBData
遵循相同的接口。我能够通过从这个基本接口扩展它们来做到这一点:
...
type Value = StringValue | NumberValue
interface BaseFormData {
[property: string]: Value
}
interface FormAData extends BaseFormData {
...
您应该将泛型声明扩展到“表单”接口本身。
在这种情况下,您需要为 TypeScript 提供一种方法来“推断”form
的 data
属性 的类型,以便 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 使用 infer 的 Form
泛型(字面意思)稍后 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"];
}
编辑:进一步思考 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"];
}
我正在尝试编写一个从给定对象获取嵌套值的 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
解决方案
这是我最后做的,类似于@jered 在他的回答中建议的:
基本上,该课程是明确您输入的任何不变量。就我而言,我并没有正式告诉编译器 every 属性 FormAData
和 FormBData
遵循相同的接口。我能够通过从这个基本接口扩展它们来做到这一点:
...
type Value = StringValue | NumberValue
interface BaseFormData {
[property: string]: Value
}
interface FormAData extends BaseFormData {
...
您应该将泛型声明扩展到“表单”接口本身。
在这种情况下,您需要为 TypeScript 提供一种方法来“推断”form
的 data
属性 的类型,以便 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 使用 infer 的 Form
泛型(字面意思)稍后 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"];
}
编辑:进一步思考 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"];
}