创建 TypeScript 索引类型,其属性“[key of Y]: Partial<X>”,当每个属性与“default: Partial<X>”属性 组合时,不是部分索引类型

Create TypeScript index type whose properties `[key of Y]: Partial<X>`, when each combined with a `default: Partial<X>` property, are not partials

考虑 TypeScript 中的以下类型定义:

enum Environment {
  Local = 'local',
  Prod = 'prod'
}

type EnvironmentConfig = {
  isCustomerFacing: boolean,
  serverUrl: string
}

type DefaultBaseConfig<T> = {
  default: T
}

type EnvironmentBaseConfig<T> = {
  [key in Environment]: T
}

type BaseConfig<T> = DefaultBaseConfig<T> | EnvironmentBaseConfig<T>;

// const baseConfig: ??? = {
const baseConfig: BaseConfig<Partial<EnvironmentConfig>> = {
  default: {
    isCustomerFacing: false
  },
  local: {
    serverUrl: 'https://local.example.com'
  },
  prod: {
    isCustomerFacing: true
  }
};

注意末尾的对象 const baseConfig: ??? 和下一行的 Partial<>。我真正想要的是 baseConfig 允许每个环境键 属性 成为 Partial<EnvironmentConfig>,并允许 default 属性 相同,但也要求 defaulteach 环境 localprod 之间的交集依次必须是完整的(不是 Partial<>)EnvironmentConfig,从而满足某些类型的要求 BaseConfig

在这里的示例中,local 是有效的,因为当与 default 结合使用时,它具有两个属性。但是,prod 将无效,因为在 defaultprod.

的交集之间没有声明 serverUrl

显然,稍后,此配置对象将有条件地合并到一些代码中,这些代码采用 BaseConfigenvironmentName 以及 returns 和 EnvironmentConfig,如果给它一个已经被 Typescript 检查以符合所需约束的静态配置对象,它将保证在运行时工作。

我对此困惑了一段时间,现在卡住了。我知道如何执行条件类型和类型约束,例如 T extends U ? T : never,但似乎无法弄清楚如何以及在何处将其应用于这种情况。

我怎样才能实现这个目标?

这是我迄今为止最好的尝试,但是,当然它不起作用:

type SplitWithDefault<
  TComplete,
  TDefault extends Partial<TComplete>,
  TSplit extends { default: TDefault, [key: string]: Partial<TComplete> }
> = { default: TDefault }
  & { [P in keyof Omit<TSplit, 'default'>]: (TSplit[P] & TDefault) extends TComplete ? TSplit[P] : never };

您想要的 BaseConfig 实际上更像是 self-referential generic constraint 而不是 TypeScript 中的特定类型。也就是说,给定一个特定的 candidate 类型 T,您可以检查它是否可分配给 BaseConfigConstraint<T> 规则,但您会发现它 hard/impossible 在单个 TypeScript 对象类型中表达“遵守此规则的所有类型”。

在这种情况下,我通常会编写一个辅助身份函数,它接受一个参数和 returns 它,但只接受 T extends BaseConfigConstraint<T> 类型的参数以获得 [=14= 的合适定义].像这样:

const asBaseConfig = <T extends
  { default: Partial<EnvironmentConfig> } &
  Record<Environment,
    Partial<EnvironmentConfig> &
    Omit<EnvironmentConfig, keyof T['default']>
  >
>(baseConfig: T) => baseConfig;

这里我们说 baseConfig 必须属于 T 类型:

  • default 属性 Partial<EnvironmentConfig> 的子类型,以及
  • 属性在 Environment 两个子类型的键:
    • Partial<EnvironmentConfig>,以及
    • 具有 Tdefault 属性 中缺少的 EnvironmentConfig 任何属性的对象。

换句话说,T 必须具有来自 Environment 以及 "default" 的键,所有这些都必须具有 Partial<EnvironmentConfig> 属性,但是default 属性 必须 出现在 Environment 个中。

让我们看看它在您的示例中是如何工作的:

const baseConfig = asBaseConfig({
  default: {
    isCustomerFacing: false
  },
  local: {
    serverUrl: 'https://local.example.com'
  },
  prod: { // error!
//~~~~ <-- Property 'serverUrl' is missing 
    isCustomerFacing: true,
  }
});

这正是您想要的错误。您可以通过将 serverUrl 添加到 proddefault 来修复错误。这样就好了。

请注意,使用约束而不是类型意味着您要提供参数的任何函数或类型或 BaseConfig 类型的 属性 现在需要是 generic 具有对应于此约束的类型参数的函数或类型。这可能是也可能不是您愿意在代码库中做的事情。

Playground link to code