链接到TS游乐场
我有一个名为genWizardDefaultState的函数,用于生成表单的默认状态。它是递归的,因此可以支持嵌套表单。我正在尝试创建一个递归泛型,以便该函数实际返回一个有用的、类型化的输出。
函数本身接受一个类似json的对象,其值只能是Args,并返回一个具有相同结构的对象,其值为Fields。参数是这样定义的:
interface TextArg {
fieldType: "text";
}
interface NumberArg {
fieldType: "number";
}
interface SelectArg {
fieldType: "select";
}
type Arg = TextArg | NumberArg | SelectArg;
和字段是这样定义的:
interface TextField {
value: string;
errors: string[];
}
interface NumberField {
value: number;
errors: string[];
}
interface SelectField {
value: any;
label: string;
errors: string[];
}
更通用的Field类型是泛型的:它接受一个Arg并根据fieldType属性返回一个特定的Field:
interface ArgToFieldMap {
text: TextField;
number: NumberField;
select: SelectField;
boolean: BooleanField;
date: DateField;
}
type Field<T extends Arg> = ArgToFieldMap[T["fieldType"]];
这适用于平面对象输入(只要我在输入类型上使用as const断言,以便typescript实际上将fieldType推断为字符串字面值而不仅仅是字符串):
type FormFields<T extends Record<keyof T, Arg>> = {
[Key in keyof T]: Field<T[Key]>;
};
例如,这段代码确实返回正确的类型!
const exampleArgsObject = {
name: { fieldType: "text" },
age: { fieldType: "number" },
favoriteTowel: { fieldType: "select" },
} as const;
type formStateType = FormFields<typeof exampleArgsObject>;
但是当涉及到使这种类型递归时,我遇到了瓶颈。我想我需要:
- 告诉FormFields它的输入类型的属性是
Arg
或Array<FormField>
。 - 使用条件类型作为映射的输出属性,以区分
Arg
和Array<FormField>
输入属性
这是我目前想到的:
type FormFieldsRecursive<T extends Record<keyof T, Arg | Array<FormFieldsRecursive<T>>>> = {
[Key in keyof T]: T[Key] extends Arg ? Field<T[Key]> : 'idk what goes here'
};
知道我应该返回什么而不是"idk这里是什么"吗?我知道它应该是某种泛型返回一个数组,但我真的很困惑。我开始怀疑这是否可能!
首先,让我们省略形式为T extends Record<keyof T, Arg | Array<FormFieldsRecursive<T>>>
的FormFields<T>
的一般约束。
该版本不起作用,因为FormFieldsRecursive<T>
是类型转换的输出。如果你真的想给输入一个名字,它可能看起来像这样:
type ArgsObj = { [k: string]: Arg | ReadonlyArray<ArgsObj> };
这里我说的是ArgsObj
有一个字符串索引签名,所以我们不关心它的密钥。(这实际上将无法匹配没有显式索引签名的接口,如microsoft/TypeScript#15300所述)。然后属性是Arg
或ArgsObj
元素的数组。好吧,是一个只读数组,因为您使用的是const
断言,而这些断言会生成只读数组。常规数组是只读数组的子类型,所以这没有任何限制。
所以我们可以,如果我们想的话,定义type FormFields<T extends ArgsObj> = ...
这样你就不能传入"bad"T
.
但我不想麻烦。这只会让事情变得更复杂。如果你确实在某个地方有函数需要确保类型是ArgsObj
类型,你可以把约束放在那里。
好的,那么我们如何将FormFields<T>
写成这样递归地工作呢?我发现下面的方法将操作分成两种类型是最简单的。一个类型FormFields<T>
映射整个对象类型,而FormField<T>
将映射该对象类型的单个属性。它的核心是这样的:
type FormFields<T> = {
[K in keyof T]: FormField<T[K]>
}
type FormField<T> =
T extends Arg ? Field<T> : { [I in keyof T]: FormFields<T[I]> };
所以FormFields<T>
是一个简单的映射类型,它将T
的每个属性映射到FormField<>
。FormField<T>
是一个条件类型,它检查属性是否为Arg
。如果是,它像以前一样执行Field<T>
。如果不是,那么我们知道它必须是一个数组类型,它包含适合FormFields<>
的类型,在这种情况下,我们使用支持将数组/元组映射到具有映射类型的数组/元组,将T
中类似数字索引的每个元素I
转换为FormFields<T[I]>
。
我们可以检查这是否有效:
const exampleArgsObjectRecursive = {
name: { fieldType: "text" },
age: { fieldType: "number" },
favTowel: { fieldType: "select" },
likesKanye: { fieldType: "boolean" },
subForms: [
{
shoeColor: { fieldType: "text" },
},
{
shoeColor: { fieldType: "text" },
},
]
} as const;
type GeneratedRecursiveExampleFieldsType = FormFields<typeof exampleArgsObjectRecursive>;
/* type GeneratedRecursiveExampleFieldsType = {
readonly name: TextField;
readonly age: NumberField;
readonly favTowel: SelectField;
readonly likesKanye: BooleanField;
readonly subForms: readonly [FormFields<{
readonly shoeColor: {
readonly fieldType: "text";
};
}>, FormFields<{
readonly shoeColor: {
readonly fieldType: "text";
};
}>];
} */
所以看起来不错,除了类型显示在FormFields<>
方面留下的东西。如果我们希望编译器实际上完全展开类型,我们可以使用另一个SO问题中所示的方法,即接受映射类型并执行额外的infer
和映射类型。一般形式是将类型XXX
转换为XXX extends infer U ? { [K in keyof U]: U[K] } : never
。这样的:
type FormFields<T> = {
[K in keyof T]: FormField<T[K]>
} extends infer U ? { [K in keyof U]: U[K] } : never;
现在我们得到
type GeneratedRecursiveExampleFieldsType = FormFields<typeof exampleArgsObjectRecursive>;
/* type GeneratedRecursiveExampleFieldsType = {
readonly name: TextField;
readonly age: NumberField;
readonly favTowel: SelectField;
readonly likesKanye: BooleanField;
readonly subForms: readonly [{
readonly shoeColor: TextField;
}, {
readonly shoeColor: TextField;
}];
} */
根据需要!
Playground链接到代码