递归补丁类型定义

Recursively patch type definition

我正在尝试编写一个泛型类型,它将类型作为参数(可以是普通对象、数组、基元等)并在它是普通对象时重新映射值类型,或者添加一些由 Configuration 类型描述的配置指令的数组。

我们称该假设修饰符为 Configurable<T>T 可以是任何复杂嵌套的实体。 Book 可以是 T 的值,例如:

type Configuration = {
  $test: {
    option1: boolean;
    option2: string;
  }
};

type Book = {
  id: string;
  title: string;
  author: string;
  related: Array<string>;
};

type Result = Configurable<Book>;

然后我希望 Configurable<Book> 正确地对以下表达式进行类型检查,其中值可以是实际值或配置对象:

const expr1: Configurable<Book> = {
  id: "1",
  title: "Harry Potter",
  author: "J.K. Rowling",
  related: ["2", "3"]
}

const expr2: Configurable<Book> = {
  id: "2",
  title: "Harry Potter",
  author: {
    $test: {
      option1: true,
      option2: "something"
    }
  },
  related: []
}

const expr3: Configurable<Book> = {
  id: "3",
  title: "Harry Potter",
  author: "J.K. Rowling",
  related: ["2", {
    $test: {
      option1: true,
      option2: "something"
    }
  }]
}

const expr4: Configurable<Book> = {
  id: "4",
  title: true, // ERROR: should be string or Configuration
  author: "J.K. Rowling",
  related: ["2", "3"]
}

const expr5: Configurable<Book> = {
  id: "5",
  title: "Harry Potter",
  author: "J.K. Rowling",
  related: {
    $test: {
      option1: true,
      option2: "something"
    }
  } // ERROR: should be an array of (string | Configuration)
}

嵌套对象或数组不应该被 Configuration 替换,只有在需要原始值的地方(参见 expr5)。 这是我尝试过的:

type Configuration = {
  $test: {
    option1: boolean;
    option2: string;
  };
};

type Configurable<T> = Record<string, any> extends T
  ? {
      [K in keyof T]: Configurable<T[K]> | Configuration;
    }
  : T extends Array<infer U>
  ? Array<Configurable<U>>
  : T;

但这会使 expr2expr3 失败。

Link to Playground

如果我正确理解您的要求,您可以使用 Configurable 的定义:

type Configurable<T> = T extends object ?
  { [K in keyof T]: Configurable<T[K]> } :
  T | Configuration;

The object type corresponds to any non-primitive, including arrays. If T extends object is not true, then T is a primitive and you want to accept T | Configuration. If T extends object is true, then you map over its properties with Configurable. This should automatically do the right thing with arrays and tuples, since mapped tuples and arrays produce tuples and arrays.

让我们试试看:

const expr1: Configurable<Book> = {
  id: "1",
  title: "Harry Potter",
  author: "J.K. Rowling",
  related: ["2", "3"]
}

const expr2: Configurable<Book> = {
  id: "2",
  title: "Harry Potter",
  author: {
    $test: {
      option1: true,
      option2: "something"
    }
  },
  related: []
}

const expr3: Configurable<Book> = {
  id: "3",
  title: "Harry Potter",
  author: "J.K. Rowling",
  related: ["2", {
    $test: {
      option1: true,
      option2: "something"
    }
  }]
}

以上示例按要求编译无错误。让我们检查错误:

const expr4: Configurable<Book> = {
  id: "4",
  title: true, // ERROR: should be string or Configuration
  author: "J.K. Rowling",
  related: ["2", "3"]
}

const expr5: Configurable<Book> = {
  id: "5",
  title: "Harry Potter",
  author: "J.K. Rowling",
  related: {
    $test: {
      option1: true,
      option2: "something"
    }
  } // ERROR: should be an array of (string | Configuration)
}

确实产生了你想要的错误。看起来不错!

Playground link to code