是否可以在打字稿中递归地将一种 JSON 类类型映射到另一种类型?

Is it possible to recursively map one JSON-like type to another in typescript?

Link to TS Playground

我有一个名为 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>;

但是在使这种类型递归时我碰壁了。我想我需要:

  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'
};

知道我应该 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 typesT 在 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;
     }];
} */

随心所欲!

Playground link to code