创建 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
属性 相同,但也要求 default
和 each 环境 local
和 prod
之间的交集依次必须是完整的(不是 Partial<>)EnvironmentConfig
,从而满足某些类型的要求 BaseConfig
。
在这里的示例中,local
是有效的,因为当与 default
结合使用时,它具有两个属性。但是,prod
将无效,因为在 default
和 prod
.
的交集之间没有声明 serverUrl
显然,稍后,此配置对象将有条件地合并到一些代码中,这些代码采用 BaseConfig
和 environmentName
以及 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>
,以及
- 具有
T
的 default
属性 中缺少的 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
添加到 prod
或 default
来修复错误。这样就好了。
请注意,使用约束而不是类型意味着您要提供参数的任何函数或类型或 BaseConfig
类型的 属性 现在需要是 generic 具有对应于此约束的类型参数的函数或类型。这可能是也可能不是您愿意在代码库中做的事情。
考虑 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
属性 相同,但也要求 default
和 each 环境 local
和 prod
之间的交集依次必须是完整的(不是 Partial<>)EnvironmentConfig
,从而满足某些类型的要求 BaseConfig
。
在这里的示例中,local
是有效的,因为当与 default
结合使用时,它具有两个属性。但是,prod
将无效,因为在 default
和 prod
.
serverUrl
显然,稍后,此配置对象将有条件地合并到一些代码中,这些代码采用 BaseConfig
和 environmentName
以及 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>
,以及- 具有
T
的default
属性 中缺少的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
添加到 prod
或 default
来修复错误。这样就好了。
请注意,使用约束而不是类型意味着您要提供参数的任何函数或类型或 BaseConfig
类型的 属性 现在需要是 generic 具有对应于此约束的类型参数的函数或类型。这可能是也可能不是您愿意在代码库中做的事情。