有可能递归地映射一个json类型到另一个在typescript?



链接到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>;

但是当涉及到使这种类型递归时,我遇到了瓶颈。我想我需要:

  1. 告诉FormFields它的输入类型的属性是ArgArray<FormField>
  2. 使用条件类型作为映射的输出属性,以区分ArgArray<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所述)。然后属性是ArgArgsObj元素的数组。好吧,是一个只读数组,因为您使用的是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链接到代码

相关内容

  • 没有找到相关文章

最新更新