是否可以在打字稿中递归地将一种 JSON 类类型映射到另一种类型?
Is it possible to recursively map one JSON-like type to another in typescript?
我有一个名为 genWizardDefaultState 的函数,用于为表单生成默认状态。它是递归的,因此它可以支持嵌套形式。我正在尝试制作一个递归泛型类型,以便该函数实际上 return 是一个有用的类型化输出。
函数本身接受一个类似JSON的对象,其值只能是Args,而returns是一个具有相同结构的对象,其值是 个字段 。 Args 定义如下:
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 和 returns 一个基于 fieldType 属性 的特定 Field:
interface ArgToFieldMap {
text: TextField;
number: NumberField;
select: SelectField;
boolean: BooleanField;
date: DateField;
}
type Field<T extends Arg> = ArgToFieldMap[T["fieldType"]];
这适用于平面对象输入(只要我在输入类型上使用 as const 断言,这样打字稿实际上将 fieldType 推断为字符串文字,而不仅仅是字符串):
type FormFields<T extends Record<keyof T, Arg>> = {
[Key in keyof T]: Field<T[Key]>;
};
例如,此代码段 return 类型正确!
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'
};
知道我应该 returning 而不是“idk what goes here”吗?
我知道它应该是某种泛型类型,return 是一些东西的数组,但我真的很困惑。我开始怀疑这是否可能!
首先,让我们去掉 T extends Record<keyof T, Arg | Array<FormFieldsRecursive<T>>>
.
形式的 FormFields<T>
上的 generic constraint
该版本不起作用,因为 FormFieldsRecursive<T>
是您的类型转换的 输出 。如果你真的想给 input 一个名字,它可能看起来像这样:
type ArgsObj = { [k: string]: Arg | ReadonlyArray<ArgsObj> };
这里我是说 ArgsObj
有一个字符串 index signature so we don't care about its keys. (This will actually fail to match interfaces without explicit index signatures, as described in microsoft/TypeScript#15300). And then the properties are either Arg
or an array of ArgsObj
elements. Well, a read-only array, since you are using const
assertions 并且那些产生 read-only 数组。常规数组是只读数组的子类型,因此这不会限制任何内容。
因此,如果需要,我们可以定义 type FormFields<T extends ArgsObj> = ...
,这样您就不会传入“错误的”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>
是一个简单的 mapped type which maps each property of T
with FormField<>
. And FormField<T>
is a conditional type that checks to see if the property is an Arg
. If so, it does Field<T>
as before. If not, then we know it must be an array type holding types appropriate for FormFields<>
, in which case we use the support for mapping arrays/tuples to arrays/tuples with mapped types 将 T
在 numeric-like 索引 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<>
方面留下了一些东西。如果我们希望编译器真正完全扩展类型,我们可以使用方法 获取映射类型并执行额外的 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;
}];
} */
随心所欲!
我有一个名为 genWizardDefaultState 的函数,用于为表单生成默认状态。它是递归的,因此它可以支持嵌套形式。我正在尝试制作一个递归泛型类型,以便该函数实际上 return 是一个有用的类型化输出。
函数本身接受一个类似JSON的对象,其值只能是Args,而returns是一个具有相同结构的对象,其值是 个字段 。 Args 定义如下:
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 和 returns 一个基于 fieldType 属性 的特定 Field:
interface ArgToFieldMap {
text: TextField;
number: NumberField;
select: SelectField;
boolean: BooleanField;
date: DateField;
}
type Field<T extends Arg> = ArgToFieldMap[T["fieldType"]];
这适用于平面对象输入(只要我在输入类型上使用 as const 断言,这样打字稿实际上将 fieldType 推断为字符串文字,而不仅仅是字符串):
type FormFields<T extends Record<keyof T, Arg>> = {
[Key in keyof T]: Field<T[Key]>;
};
例如,此代码段 return 类型正确!
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'
};
知道我应该 returning 而不是“idk what goes here”吗? 我知道它应该是某种泛型类型,return 是一些东西的数组,但我真的很困惑。我开始怀疑这是否可能!
首先,让我们去掉 T extends Record<keyof T, Arg | Array<FormFieldsRecursive<T>>>
.
FormFields<T>
上的 generic constraint
该版本不起作用,因为 FormFieldsRecursive<T>
是您的类型转换的 输出 。如果你真的想给 input 一个名字,它可能看起来像这样:
type ArgsObj = { [k: string]: Arg | ReadonlyArray<ArgsObj> };
这里我是说 ArgsObj
有一个字符串 index signature so we don't care about its keys. (This will actually fail to match interfaces without explicit index signatures, as described in microsoft/TypeScript#15300). And then the properties are either Arg
or an array of ArgsObj
elements. Well, a read-only array, since you are using const
assertions 并且那些产生 read-only 数组。常规数组是只读数组的子类型,因此这不会限制任何内容。
因此,如果需要,我们可以定义 type FormFields<T extends ArgsObj> = ...
,这样您就不会传入“错误的”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>
是一个简单的 mapped type which maps each property of T
with FormField<>
. And FormField<T>
is a conditional type that checks to see if the property is an Arg
. If so, it does Field<T>
as before. If not, then we know it must be an array type holding types appropriate for FormFields<>
, in which case we use the support for mapping arrays/tuples to arrays/tuples with mapped types 将 T
在 numeric-like 索引 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<>
方面留下了一些东西。如果我们希望编译器真正完全扩展类型,我们可以使用方法 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;
}];
} */
随心所欲!